diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 25e544ccf27b..a1427475626c 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1638,6 +1638,102 @@ diagnosticsChannel.subscribe('crypto.fips.indicator', (message) => { }); ``` +#### File system + + + +> Stability: 1 - Experimental + +Each `node:fs` operation has a [`TracingChannel`][] named `fs.`. +The callback, synchronous, and promise forms of an operation all publish to +that channel. For example, `fs.stat()`, `fs.statSync()`, and `fsPromises.stat()` +all publish to `tracing:fs.stat:start`, `tracing:fs.stat:end`, and so on. + +The form decides which events a call publishes. The synchronous form publishes +`start` and `end`. The callback and promise forms publish `start` and `end` +when the call returns, then `asyncStart` and `asyncEnd` when the callback runs +or the promise settles. A call that fails also publishes `error`. See +[TracingChannel Channels][] for the order and contents of the events. + +* `event` {Object} containing the following properties + * `api` {string} The form of the function that was called: `'callback'`, + `'sync'`, or `'promise'`. + * `args` {Array} The arguments the function was called with. For a + `FileHandle` method, the arguments of the method. + * `result` {any} The result of the operation. For the callback form, this is + the first callback argument after the error, so for `fs.read` and + `fs.write` it is the number of bytes. + * One property for each leading argument, named as in the table below. For + `FileHandle` methods, `fd` is the file descriptor of the handle. + +The table lists the functions that publish to each channel and the properties +for their leading arguments. A function name without a prefix stands for all +of its forms: `stat` means `fs.stat()`, `fs.statSync()`, and +`fsPromises.stat()`. + +| Channel | Functions | Properties | +| --------------- | ---------------------------------------------------------------- | ------------------------ | +| `fs.access` | `access` | `path` | +| `fs.appendFile` | `appendFile` | `path`, `data` | +| `fs.chmod` | `chmod` | `path`, `mode` | +| `fs.chown` | `chown` | `path`, `uid`, `gid` | +| `fs.close` | `close`, `filehandle.close()` | `fd` | +| `fs.closedir` | `dir.close()`, `dir.closeSync()` | `path` | +| `fs.copyFile` | `copyFile` | `path`, `dest` | +| `fs.cp` | `cp` | `path`, `dest` | +| `fs.exists` | `exists` | `path` | +| `fs.fchmod` | `fchmod`, `filehandle.chmod()` | `fd`, `mode` | +| `fs.fchown` | `fchown`, `filehandle.chown()` | `fd`, `uid`, `gid` | +| `fs.fdatasync` | `fdatasync`, `filehandle.datasync()` | `fd` | +| `fs.fstat` | `fstat`, `filehandle.stat()` | `fd` | +| `fs.fsync` | `fsync`, `filehandle.sync()` | `fd` | +| `fs.ftruncate` | `ftruncate`, `filehandle.truncate()` | `fd` | +| `fs.futimes` | `futimes`, `filehandle.utimes()` | `fd`, `atime`, `mtime` | +| `fs.glob` | `glob` | `pattern` | +| `fs.lchmod` | `lchmod` | `path`, `mode` | +| `fs.lchown` | `lchown` | `path`, `uid`, `gid` | +| `fs.link` | `link` | `path`, `dest` | +| `fs.lstat` | `lstat` | `path` | +| `fs.lutimes` | `lutimes` | `path`, `atime`, `mtime` | +| `fs.mkdir` | `mkdir` | `path` | +| `fs.mkdtemp` | `mkdtemp`, `mkdtempDisposable` | `prefix` | +| `fs.mkstemp` | `mkstemp` | `prefix` | +| `fs.open` | `open` | `path` | +| `fs.openAsBlob` | `openAsBlob` | `path` | +| `fs.opendir` | `opendir` | `path` | +| `fs.read` | `read`, `filehandle.read()` | `fd` | +| `fs.readdir` | `readdir` | `path` | +| `fs.readFile` | `readFile`, `filehandle.readFile()` | `path` | +| `fs.readlink` | `readlink` | `path` | +| `fs.readv` | `readv`, `filehandle.readv()` | `fd`, `buffers` | +| `fs.realpath` | `realpath`, `realpath.native` | `path` | +| `fs.rename` | `rename` | `path`, `dest` | +| `fs.rm` | `rm` | `path` | +| `fs.rmdir` | `rmdir` | `path` | +| `fs.stat` | `stat` | `path` | +| `fs.statfs` | `statfs` | `path` | +| `fs.symlink` | `symlink` | `target`, `path` | +| `fs.truncate` | `truncate` | `path` | +| `fs.unlink` | `unlink` | `path` | +| `fs.utimes` | `utimes` | `path`, `atime`, `mtime` | +| `fs.write` | `write`, `filehandle.write()` | `fd`, `buffer` | +| `fs.writeFile` | `writeFile`, `filehandle.writeFile()`, `filehandle.appendFile()` | `path`, `data` | +| `fs.writev` | `writev`, `filehandle.writev()` | `fd`, `buffers` | + +`fs.readFile`, `fs.writeFile`, and `fs.appendFile` accept a file descriptor or +a {FileHandle} in place of a path. `path` then holds that descriptor or handle. +When `fs.write` is called with a string, `buffer` holds that string. + +An operation that is implemented with other `fs` operations publishes those +too, between its own `start` and `end`. For example, `fs.readFileSync()` without +an encoding calls `fs.openSync()`, `fs.readSync()`, and `fs.closeSync()`, so it +publishes `fs.open`, `fs.read`, and `fs.close` events inside its `fs.readFile` +events. The streams from `fs.createReadStream()` and `fs.createWriteStream()` +call `fs.open()`, `fs.read()`, `fs.write()`, `fs.writev()`, and `fs.close()`, +which publish as usual. + #### HTTP > Stability: 1 - Experimental diff --git a/lib/fs.js b/lib/fs.js index c0f7e6f0adf7..b5ad44d0674e 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -26,6 +26,7 @@ const { ArrayPrototypePush, + ArrayPrototypeSlice, BigIntPrototypeToString, FunctionPrototypeCall, MathMax, @@ -131,6 +132,54 @@ const { validateStringAfterArrayBufferView, vfsState, warnOnNonPortableTemplate, + fsAccess, + fsAppendFile, + fsChmod, + fsChown, + fsClose, + fsCopyFile, + fsCp, + fsExists, + fsFchmod, + fsFchown, + fsFdatasync, + fsFstat, + fsFsync, + fsFtruncate, + fsFutimes, + fsGlob, + fsLchmod, + fsLchown, + fsLink, + fsLstat, + fsLutimes, + fsMkdir, + fsMkdtemp, + fsMkstemp, + fsOpen, + fsOpenAsBlob, + fsRead, + fsReadFile, + fsReaddir, + fsReadlink, + fsReadv, + fsRealpath, + fsRename, + fsRm, + fsRmdir, + fsStat, + fsStatfs, + fsSymlink, + fsTruncate, + fsUnlink, + fsUtimes, + fsWrite, + fsWriteFile, + fsWritev, + kTraced, + traceSync, + traceCallback, + tracePromise, } = require('internal/fs/utils'); const { CHAR_FORWARD_SLASH, @@ -241,6 +290,10 @@ function isFileType(stats, fileType) { * @returns {void} */ function access(path, mode, callback) { + if (this !== kTraced && fsAccess.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsAccess, context, access, arguments); + } if (typeof mode === 'function') { callback = mode; mode = F_OK; @@ -265,6 +318,10 @@ function access(path, mode, callback) { * @returns {void} */ function accessSync(path, mode) { + if (this !== kTraced && fsAccess.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsAccess, context, accessSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.accessSync(path, mode); @@ -281,6 +338,12 @@ function accessSync(path, mode) { */ function exists(path, callback) { validateFunction(callback, 'cb'); + if (this !== kTraced && fsExists.hasSubscribers) { + // The callback of fs.exists() takes only the result. + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsExists, context, existsErrorFirst, + [path, (err, result) => callback(result)]); + } const h = vfsState.handlers; if (h !== null) { @@ -302,6 +365,10 @@ function exists(path, callback) { } } +function existsErrorFirst(path, callback) { + FunctionPrototypeCall(exists, this, path, (result) => callback(null, result)); +} + ObjectDefineProperty(exists, kCustomPromisifiedSymbol, { __proto__: null, value: function exists(path) { @@ -316,6 +383,10 @@ let showExistsDeprecation = true; * @returns {boolean} */ function existsSync(path) { + if (this !== kTraced && fsExists.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsExists, context, existsSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.existsSync(path); @@ -419,6 +490,10 @@ function bindSignalToReq(req, signal, callback) { * @returns {void} */ function readFile(path, options, callback) { + if (this !== kTraced && fsReadFile.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsReadFile, context, readFile, arguments); + } callback ||= options; validateFunction(callback, 'cb'); @@ -602,6 +677,10 @@ function tryReadSyncWithUserBuffer(fd, isUserFd, buffer, byteLengthName) { * @returns {string | Buffer} */ function readFileSync(path, options) { + if (this !== kTraced && fsReadFile.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsReadFile, context, readFileSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.readFileSync(path, options); @@ -701,6 +780,11 @@ function defaultCloseCallback(err) { * @returns {void} */ function close(fd, callback = defaultCloseCallback) { + if (this !== kTraced && fsClose.hasSubscribers) { + // Passes the default callback, so that the call has one to trace. + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsClose, context, close, [fd, callback]); + } if (callback !== defaultCloseCallback) callback = makeCallback(callback); @@ -718,6 +802,10 @@ function close(fd, callback = defaultCloseCallback) { * @returns {void} */ function closeSync(fd) { + if (this !== kTraced && fsClose.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsClose, context, closeSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.closeSync(fd); @@ -738,6 +826,10 @@ function closeSync(fd) { * @returns {void} */ function open(path, flags, mode, callback) { + if (this !== kTraced && fsOpen.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsOpen, context, open, arguments); + } if (arguments.length < 3) { callback = flags; flags = 'r'; @@ -771,6 +863,10 @@ function open(path, flags, mode, callback) { * @returns {number} */ function openSync(path, flags, mode) { + if (this !== kTraced && fsOpen.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsOpen, context, openSync, arguments); + } flags = stringToFlags(flags); mode = parseFileMode(mode, 'mode', 0o666); const h = vfsState.handlers; @@ -793,6 +889,10 @@ function openSync(path, flags, mode) { * @returns {Promise} */ function openAsBlob(path, options = kEmptyObject) { + if (this !== kTraced && fsOpenAsBlob.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return tracePromise(fsOpenAsBlob, context, openAsBlob, arguments); + } validateObject(options, 'options'); const { type = '' } = options; validateString(type, 'options.type'); @@ -819,6 +919,10 @@ function openAsBlob(path, options = kEmptyObject) { * @returns {Blob} */ function openAsBlobSync(path, options = kEmptyObject) { + if (this !== kTraced && fsOpenAsBlob.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsOpenAsBlob, context, openAsBlobSync, arguments); + } validateObject(options, 'options'); const { type = '' } = options; validateString(type, 'options.type'); @@ -853,6 +957,10 @@ function openAsBlobSync(path, options = kEmptyObject) { * @returns {void} */ function read(fd, buffer, offsetOrOptions, length, position, callback) { + if (this !== kTraced && fsRead.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsRead, context, read, arguments); + } fd = getValidatedFd(fd); let offset = offsetOrOptions; let params = null; @@ -955,6 +1063,10 @@ ObjectDefineProperty(read, kCustomPromisifyArgsSymbol, * @returns {number} */ function readSync(fd, buffer, offsetOrOptions, length, position) { + if (this !== kTraced && fsRead.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsRead, context, readSync, arguments); + } validateBuffer(buffer); let offset = offsetOrOptions; @@ -1018,6 +1130,10 @@ function readSync(fd, buffer, offsetOrOptions, length, position) { * @returns {void} */ function readv(fd, buffers, position, callback) { + if (this !== kTraced && fsReadv.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, buffers }; + return traceCallback(fsReadv, context, readv, arguments); + } function wrapper(err, read) { callback(err, read || 0, buffers); } @@ -1059,6 +1175,10 @@ ObjectDefineProperty(readv, kCustomPromisifyArgsSymbol, * @returns {number} */ function readvSync(fd, buffers, position) { + if (this !== kTraced && fsReadv.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, buffers }; + return traceSync(fsReadv, context, readvSync, arguments); + } validateBufferArray(buffers); if (typeof position !== 'number') @@ -1088,6 +1208,10 @@ function readvSync(fd, buffers, position) { * @returns {void} */ function write(fd, buffer, offsetOrOptions, length, position, callback) { + if (this !== kTraced && fsWrite.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, buffer }; + return traceCallback(fsWrite, context, write, arguments); + } function wrapper(err, written) { // Retain a reference to buffer so that it can't be GC'ed too soon. callback(err, written || 0, buffer); @@ -1185,6 +1309,10 @@ ObjectDefineProperty(write, kCustomPromisifyArgsSymbol, * @returns {number} */ function writeSync(fd, buffer, offsetOrOptions, length, position) { + if (this !== kTraced && fsWrite.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, buffer }; + return traceSync(fsWrite, context, writeSync, arguments); + } const ctx = {}; let result; @@ -1251,6 +1379,10 @@ function writeSync(fd, buffer, offsetOrOptions, length, position) { * @returns {void} */ function writev(fd, buffers, position, callback) { + if (this !== kTraced && fsWritev.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, buffers }; + return traceCallback(fsWritev, context, writev, arguments); + } function wrapper(err, written) { callback(err, written || 0, buffers); } @@ -1299,6 +1431,10 @@ ObjectDefineProperty(writev, kCustomPromisifyArgsSymbol, { * @returns {number} */ function writevSync(fd, buffers, position) { + if (this !== kTraced && fsWritev.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, buffers }; + return traceSync(fsWritev, context, writevSync, arguments); + } validateBufferArray(buffers); if (buffers.length === 0) { @@ -1326,6 +1462,10 @@ function writevSync(fd, buffers, position) { * @returns {void} */ function rename(oldPath, newPath, callback) { + if (this !== kTraced && fsRename.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path: oldPath, dest: newPath }; + return traceCallback(fsRename, context, rename, arguments); + } const h = vfsState.handlers; if (h !== null && vfsVoid(h.rename(oldPath, newPath), callback)) return; @@ -1348,6 +1488,10 @@ function rename(oldPath, newPath, callback) { * @returns {void} */ function renameSync(oldPath, newPath) { + if (this !== kTraced && fsRename.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path: oldPath, dest: newPath }; + return traceSync(fsRename, context, renameSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.renameSync(oldPath, newPath); @@ -1367,6 +1511,10 @@ function renameSync(oldPath, newPath) { * @returns {void} */ function truncate(path, len, callback) { + if (this !== kTraced && fsTruncate.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsTruncate, context, truncate, arguments); + } if (typeof len === 'function') { callback = len; len = 0; @@ -1400,6 +1548,10 @@ function truncate(path, len, callback) { * @returns {void} */ function truncateSync(path, len) { + if (this !== kTraced && fsTruncate.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsTruncate, context, truncateSync, arguments); + } if (len === undefined) { len = 0; } @@ -1427,6 +1579,10 @@ function truncateSync(path, len) { * @returns {void} */ function ftruncate(fd, len = 0, callback) { + if (this !== kTraced && fsFtruncate.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsFtruncate, context, ftruncate, arguments); + } if (typeof len === 'function') { callback = len; len = 0; @@ -1450,6 +1606,10 @@ function ftruncate(fd, len = 0, callback) { * @returns {void} */ function ftruncateSync(fd, len = 0) { + if (this !== kTraced && fsFtruncate.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsFtruncate, context, ftruncateSync, arguments); + } validateInteger(len, 'len'); const h = vfsState.handlers; @@ -1482,6 +1642,10 @@ function lazyLoadRimraf() { * @returns {void} */ function rmdir(path, options, callback) { + if (this !== kTraced && fsRmdir.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsRmdir, context, rmdir, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -1518,6 +1682,10 @@ function rmdir(path, options, callback) { * @returns {void} */ function rmdirSync(path, options) { + if (this !== kTraced && fsRmdir.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsRmdir, context, rmdirSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.rmdirSync(path); @@ -1551,6 +1719,10 @@ function rmdirSync(path, options) { * @returns {void} */ function rm(path, options, callback) { + if (this !== kTraced && fsRm.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsRm, context, rm, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -1583,6 +1755,10 @@ function rm(path, options, callback) { * @returns {void} */ function rmSync(path, options) { + if (this !== kTraced && fsRm.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsRm, context, rmSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.rmSync(path, options); @@ -1601,6 +1777,10 @@ function rmSync(path, options) { * @returns {void} */ function fdatasync(fd, callback) { + if (this !== kTraced && fsFdatasync.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsFdatasync, context, fdatasync, arguments); + } callback = makeCallback(callback); const h = vfsState.handlers; @@ -1624,6 +1804,10 @@ function fdatasync(fd, callback) { * @returns {void} */ function fdatasyncSync(fd) { + if (this !== kTraced && fsFdatasync.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsFdatasync, context, fdatasyncSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.fdatasyncSync(fd); @@ -1644,6 +1828,10 @@ function fdatasyncSync(fd) { * @returns {void} */ function fsync(fd, callback) { + if (this !== kTraced && fsFsync.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsFsync, context, fsync, arguments); + } callback = makeCallback(callback); const h = vfsState.handlers; @@ -1665,6 +1853,10 @@ function fsync(fd, callback) { * @returns {void} */ function fsyncSync(fd) { + if (this !== kTraced && fsFsync.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsFsync, context, fsyncSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.fsyncSync(fd); @@ -1688,6 +1880,10 @@ function fsyncSync(fd) { * @returns {void} */ function mkdir(path, options, callback) { + if (this !== kTraced && fsMkdir.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsMkdir, context, mkdir, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -1737,6 +1933,10 @@ function mkdir(path, options, callback) { * @returns {string | void} */ function mkdirSync(path, options) { + if (this !== kTraced && fsMkdir.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsMkdir, context, mkdirSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const vfsResult = h.mkdirSync(path, options); @@ -1885,6 +2085,10 @@ function readdirSyncRecursive(basePath, options) { * @returns {void} */ function readdir(path, options, callback) { + if (this !== kTraced && fsReaddir.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsReaddir, context, readdir, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -1936,6 +2140,10 @@ function readdir(path, options, callback) { * @returns {string | Buffer[] | Dirent[]} */ function readdirSync(path, options) { + if (this !== kTraced && fsReaddir.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsReaddir, context, readdirSync, arguments); + } options = getOptions(options); path = getValidatedPath(path); if (options.recursive != null) { @@ -1973,6 +2181,10 @@ function readdirSync(path, options) { * @returns {void} */ function fstat(fd, options = { __proto__: null, bigint: false }, callback) { + if (this !== kTraced && fsFstat.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd }; + return traceCallback(fsFstat, context, fstat, arguments); + } if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2005,6 +2217,10 @@ function fstat(fd, options = { __proto__: null, bigint: false }, callback) { * @returns {void} */ function lstat(path, options = { __proto__: null, bigint: false }, callback) { + if (this !== kTraced && fsLstat.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsLstat, context, lstat, arguments); + } if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2045,6 +2261,10 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) { * @returns {void} */ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }, callback) { + if (this !== kTraced && fsStat.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsStat, context, stat, arguments); + } if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2069,6 +2289,10 @@ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: } function statfs(path, options = { __proto__: null, bigint: false }, callback) { + if (this !== kTraced && fsStatfs.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsStatfs, context, statfs, arguments); + } if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2110,6 +2334,10 @@ function statfs(path, options = { __proto__: null, bigint: false }, callback) { * @returns {Stats | undefined} */ function fstatSync(fd, options = { __proto__: null, bigint: false }) { + if (this !== kTraced && fsFstat.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd }; + return traceSync(fsFstat, context, fstatSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.fstatSync(fd); @@ -2133,6 +2361,10 @@ function fstatSync(fd, options = { __proto__: null, bigint: false }) { * @returns {Stats | undefined} */ function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (this !== kTraced && fsLstat.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsLstat, context, lstatSync, arguments); + } const h = vfsState.handlers; if (h !== null) { try { @@ -2173,6 +2405,10 @@ function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEn * @returns {Stats} */ function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (this !== kTraced && fsStat.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsStat, context, statSync, arguments); + } const h = vfsState.handlers; if (h !== null) { try { @@ -2196,6 +2432,10 @@ function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEnt } function statfsSync(path, options = { __proto__: null, bigint: false }) { + if (this !== kTraced && fsStatfs.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsStatfs, context, statfsSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.statfsSync(path, options); @@ -2218,6 +2458,10 @@ function statfsSync(path, options = { __proto__: null, bigint: false }) { * @returns {void} */ function readlink(path, options, callback) { + if (this !== kTraced && fsReadlink.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsReadlink, context, readlink, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -2241,6 +2485,10 @@ function readlink(path, options, callback) { * @returns {string | Buffer} */ function readlinkSync(path, options) { + if (this !== kTraced && fsReadlink.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsReadlink, context, readlinkSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.readlinkSync(path, options); @@ -2259,6 +2507,10 @@ function readlinkSync(path, options) { * @returns {void} */ function symlink(target, path, type, callback) { + if (this !== kTraced && fsSymlink.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), target, path }; + return traceCallback(fsSymlink, context, symlink, arguments); + } if (callback === undefined) { callback = makeCallback(type); type = undefined; @@ -2331,6 +2583,10 @@ function symlink(target, path, type, callback) { * @returns {void} */ function symlinkSync(target, path, type) { + if (this !== kTraced && fsSymlink.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), target, path }; + return traceSync(fsSymlink, context, symlinkSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.symlinkSync(target, path, type); @@ -2371,6 +2627,10 @@ function symlinkSync(target, path, type) { * @returns {void} */ function link(existingPath, newPath, callback) { + if (this !== kTraced && fsLink.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path: existingPath, dest: newPath }; + return traceCallback(fsLink, context, link, arguments); + } callback = makeCallback(callback); const h = vfsState.handlers; @@ -2393,6 +2653,10 @@ function link(existingPath, newPath, callback) { * @returns {void} */ function linkSync(existingPath, newPath) { + if (this !== kTraced && fsLink.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path: existingPath, dest: newPath }; + return traceSync(fsLink, context, linkSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.linkSync(existingPath, newPath); @@ -2415,6 +2679,10 @@ function linkSync(existingPath, newPath) { * @returns {void} */ function unlink(path, callback) { + if (this !== kTraced && fsUnlink.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsUnlink, context, unlink, arguments); + } const h = vfsState.handlers; if (h !== null && vfsVoid(h.unlink(path), callback)) return; @@ -2430,6 +2698,10 @@ function unlink(path, callback) { * @returns {void} */ function unlinkSync(path) { + if (this !== kTraced && fsUnlink.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsUnlink, context, unlinkSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.unlinkSync(path); @@ -2446,6 +2718,10 @@ function unlinkSync(path) { * @returns {void} */ function fchmod(fd, mode, callback) { + if (this !== kTraced && fsFchmod.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, mode }; + return traceCallback(fsFchmod, context, fchmod, arguments); + } mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); @@ -2469,6 +2745,10 @@ function fchmod(fd, mode, callback) { * @returns {void} */ function fchmodSync(fd, mode) { + if (this !== kTraced && fsFchmod.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, mode }; + return traceSync(fsFchmod, context, fchmodSync, arguments); + } mode = parseFileMode(mode, 'mode'); const h = vfsState.handlers; @@ -2491,6 +2771,10 @@ function fchmodSync(fd, mode) { * @returns {void} */ function lchmod(path, mode, callback) { + if (this !== kTraced && fsLchmod.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, mode }; + return traceCallback(fsLchmod, context, lchmod, arguments); + } validateFunction(callback, 'cb'); mode = parseFileMode(mode, 'mode'); @@ -2519,6 +2803,10 @@ function lchmod(path, mode, callback) { * @returns {void} */ function lchmodSync(path, mode) { + if (this !== kTraced && fsLchmod.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, mode }; + return traceSync(fsLchmod, context, lchmodSync, arguments); + } const fd = fs.openSync(path, O_WRONLY | O_SYMLINK); // Prefer to return the chmod error, if one occurs, @@ -2538,6 +2826,10 @@ function lchmodSync(path, mode) { * @returns {void} */ function chmod(path, mode, callback) { + if (this !== kTraced && fsChmod.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, mode }; + return traceCallback(fsChmod, context, chmod, arguments); + } path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); @@ -2557,6 +2849,10 @@ function chmod(path, mode, callback) { * @returns {void} */ function chmodSync(path, mode) { + if (this !== kTraced && fsChmod.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, mode }; + return traceSync(fsChmod, context, chmodSync, arguments); + } path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); @@ -2578,6 +2874,10 @@ function chmodSync(path, mode) { * @returns {void} */ function lchown(path, uid, gid, callback) { + if (this !== kTraced && fsLchown.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return traceCallback(fsLchown, context, lchown, arguments); + } callback = makeCallback(callback); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); @@ -2599,6 +2899,10 @@ function lchown(path, uid, gid, callback) { * @returns {void} */ function lchownSync(path, uid, gid) { + if (this !== kTraced && fsLchown.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return traceSync(fsLchown, context, lchownSync, arguments); + } path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2621,6 +2925,10 @@ function lchownSync(path, uid, gid) { * @returns {void} */ function fchown(fd, uid, gid, callback) { + if (this !== kTraced && fsFchown.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, uid, gid }; + return traceCallback(fsFchown, context, fchown, arguments); + } validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); callback = makeCallback(callback); @@ -2646,6 +2954,10 @@ function fchown(fd, uid, gid, callback) { * @returns {void} */ function fchownSync(fd, uid, gid) { + if (this !== kTraced && fsFchown.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, uid, gid }; + return traceSync(fsFchown, context, fchownSync, arguments); + } validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2672,6 +2984,10 @@ function fchownSync(fd, uid, gid) { * @returns {void} */ function chown(path, uid, gid, callback) { + if (this !== kTraced && fsChown.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return traceCallback(fsChown, context, chown, arguments); + } callback = makeCallback(callback); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); @@ -2694,6 +3010,10 @@ function chown(path, uid, gid, callback) { * @returns {void} */ function chownSync(path, uid, gid) { + if (this !== kTraced && fsChown.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return traceSync(fsChown, context, chownSync, arguments); + } path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2717,6 +3037,10 @@ function chownSync(path, uid, gid) { * @returns {void} */ function utimes(path, atime, mtime, callback) { + if (this !== kTraced && fsUtimes.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return traceCallback(fsUtimes, context, utimes, arguments); + } callback = makeCallback(callback); path = getValidatedPath(path); atime = toUnixTimestamp(atime); @@ -2739,6 +3063,10 @@ function utimes(path, atime, mtime, callback) { * @returns {void} */ function utimesSync(path, atime, mtime) { + if (this !== kTraced && fsUtimes.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return traceSync(fsUtimes, context, utimesSync, arguments); + } path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2762,6 +3090,10 @@ function utimesSync(path, atime, mtime) { * @returns {void} */ function futimes(fd, atime, mtime, callback) { + if (this !== kTraced && fsFutimes.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), fd, atime, mtime }; + return traceCallback(fsFutimes, context, futimes, arguments); + } atime = toUnixTimestamp(atime, 'atime'); mtime = toUnixTimestamp(mtime, 'mtime'); callback = makeCallback(callback); @@ -2789,6 +3121,10 @@ function futimes(fd, atime, mtime, callback) { * @returns {void} */ function futimesSync(fd, atime, mtime) { + if (this !== kTraced && fsFutimes.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), fd, atime, mtime }; + return traceSync(fsFutimes, context, futimesSync, arguments); + } atime = toUnixTimestamp(atime, 'atime'); mtime = toUnixTimestamp(mtime, 'mtime'); @@ -2815,6 +3151,10 @@ function futimesSync(fd, atime, mtime) { * @returns {void} */ function lutimes(path, atime, mtime, callback) { + if (this !== kTraced && fsLutimes.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return traceCallback(fsLutimes, context, lutimes, arguments); + } callback = makeCallback(callback); path = getValidatedPath(path); atime = toUnixTimestamp(atime); @@ -2837,6 +3177,10 @@ function lutimes(path, atime, mtime, callback) { * @returns {void} */ function lutimesSync(path, atime, mtime) { + if (this !== kTraced && fsLutimes.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return traceSync(fsLutimes, context, lutimesSync, arguments); + } path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2919,6 +3263,10 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) * @returns {void} */ function writeFile(path, data, options, callback) { + if (this !== kTraced && fsWriteFile.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, data }; + return traceCallback(fsWriteFile, context, writeFile, arguments); + } callback ||= options; validateFunction(callback, 'cb'); @@ -2996,6 +3344,10 @@ function writeFile(path, data, options, callback) { * @returns {void} */ function writeFileSync(path, data, options) { + if (this !== kTraced && fsWriteFile.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, data }; + return traceSync(fsWriteFile, context, writeFileSync, arguments); + } options = getOptions(options, { encoding: 'utf8', mode: 0o666, @@ -3071,6 +3423,10 @@ function writeFileSync(path, data, options) { * @returns {void} */ function appendFile(path, data, options, callback) { + if (this !== kTraced && fsAppendFile.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path, data }; + return traceCallback(fsAppendFile, context, appendFile, arguments); + } callback ||= options; validateFunction(callback, 'cb'); @@ -3107,6 +3463,10 @@ function appendFile(path, data, options, callback) { * @returns {void} */ function appendFileSync(path, data, options) { + if (this !== kTraced && fsAppendFile.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path, data }; + return traceSync(fsAppendFile, context, appendFileSync, arguments); + } options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); parseFileMode(options.mode, 'mode', 0o666); @@ -3355,6 +3715,10 @@ if (isWindows) { * @returns {string | Buffer} */ function realpathSync(p, options) { + if (this !== kTraced && fsRealpath.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path: p }; + return traceSync(fsRealpath, context, realpathSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.realpathSync(p, options); @@ -3503,7 +3867,11 @@ function realpathSync(p, options) { * @param {string | { encoding?: string; }} [options] * @returns {string | Buffer} */ -realpathSync.native = (path, options) => { +realpathSync.native = function native(path, options) { + if (this !== kTraced && fsRealpath.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsRealpath, context, native, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.realpathSync(path, options); @@ -3528,6 +3896,10 @@ realpathSync.native = (path, options) => { * @returns {void} */ function realpath(p, options, callback) { + if (this !== kTraced && fsRealpath.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path: p }; + return traceCallback(fsRealpath, context, realpath, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -3680,7 +4052,11 @@ function realpath(p, options, callback) { * ) => any} callback * @returns {void} */ -realpath.native = (path, options, callback) => { +realpath.native = function native(path, options, callback) { + if (this !== kTraced && fsRealpath.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsRealpath, context, native, arguments); + } callback = makeCallback(callback || options); const h = vfsState.handlers; if (h !== null && vfsResult(h.realpath(path, options), callback)) return; @@ -3702,6 +4078,10 @@ realpath.native = (path, options, callback) => { * @returns {void} */ function mkdtemp(prefix, options, callback) { + if (this !== kTraced && fsMkdtemp.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), prefix }; + return traceCallback(fsMkdtemp, context, mkdtemp, arguments); + } callback = makeCallback(typeof options === 'function' ? options : callback); const h = vfsState.handlers; @@ -3726,6 +4106,10 @@ function mkdtemp(prefix, options, callback) { * @returns {string} */ function mkdtempSync(prefix, options) { + if (this !== kTraced && fsMkdtemp.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), prefix }; + return traceSync(fsMkdtemp, context, mkdtempSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.mkdtempSync(prefix, options); @@ -3750,6 +4134,10 @@ function mkdtempSync(prefix, options) { * @returns {object} A disposable object with a "path" property. */ function mkdtempDisposableSync(prefix, options) { + if (this !== kTraced && fsMkdtemp.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), prefix }; + return traceSync(fsMkdtemp, context, mkdtempDisposableSync, arguments); + } options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -3781,6 +4169,10 @@ function mkdtempDisposableSync(prefix, options) { * @returns {void} */ function mkstemp(prefix, options, callback) { + if (this !== kTraced && fsMkstemp.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), prefix }; + return traceCallback(fsMkstemp, context, mkstemp, arguments); + } callback = makeCallback(typeof options === 'function' ? options : callback); options = getOptions(options); @@ -3808,6 +4200,10 @@ function mkstemp(prefix, options, callback) { * @returns {{ path: string | Buffer, fd: number }} */ function mkstempSync(prefix, options) { + if (this !== kTraced && fsMkstemp.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), prefix }; + return traceSync(fsMkstemp, context, mkstempSync, arguments); + } options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -3835,6 +4231,10 @@ function mkstempSync(prefix, options) { * @returns {void} */ function copyFile(src, dest, mode, callback) { + if (this !== kTraced && fsCopyFile.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return traceCallback(fsCopyFile, context, copyFile, arguments); + } if (typeof mode === 'function') { callback = mode; mode = 0; @@ -3861,6 +4261,10 @@ function copyFile(src, dest, mode, callback) { * @returns {void} */ function copyFileSync(src, dest, mode) { + if (this !== kTraced && fsCopyFile.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return traceSync(fsCopyFile, context, copyFileSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.copyFileSync(src, dest, mode); @@ -3883,6 +4287,10 @@ function copyFileSync(src, dest, mode) { * @returns {void} */ function cp(src, dest, options, callback) { + if (this !== kTraced && fsCp.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return traceCallback(fsCp, context, cp, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -3904,6 +4312,10 @@ function cp(src, dest, options, callback) { * @returns {void} */ function cpSync(src, dest, options) { + if (this !== kTraced && fsCp.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return traceSync(fsCp, context, cpSync, arguments); + } options = validateCpOptions(options); src = getValidatedPath(src, 'src'); dest = getValidatedPath(dest, 'dest'); @@ -3979,6 +4391,10 @@ function createWriteStream(path, options) { const lazyGlob = getLazy(() => require('internal/fs/glob').Glob); function glob(pattern, options, callback) { + if (this !== kTraced && fsGlob.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), pattern }; + return traceCallback(fsGlob, context, glob, arguments); + } if (typeof options === 'function') { callback = options; options = undefined; @@ -3994,6 +4410,10 @@ function glob(pattern, options, callback) { } function globSync(pattern, options) { + if (this !== kTraced && fsGlob.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), pattern }; + return traceSync(fsGlob, context, globSync, arguments); + } const Glob = lazyGlob(); return new Glob(pattern, options).globSync(); } diff --git a/lib/internal/fs/dir.js b/lib/internal/fs/dir.js index 32050f31ae6d..e96f013f86ef 100644 --- a/lib/internal/fs/dir.js +++ b/lib/internal/fs/dir.js @@ -3,6 +3,7 @@ const { ArrayPrototypePush, ArrayPrototypeShift, + ArrayPrototypeSlice, FunctionPrototypeBind, ObjectDefineProperties, PromiseReject, @@ -32,6 +33,11 @@ const { getOptions, getValidatedPath, vfsState, + fsClosedir, + fsOpendir, + kTraced, + traceSync, + traceCallback, } = require('internal/fs/utils'); const { validateFunction, @@ -71,7 +77,7 @@ class Dir { this.#readPromisified = FunctionPrototypeBind( promisify(this.#readImpl), this, false); this.#closePromisified = FunctionPrototypeBind( - promisify(this.close), this); + promisify(this.#closeImpl), this); } get path() { @@ -246,11 +252,28 @@ class Dir { if (this.#closed === true) { return PromiseReject(new ERR_DIR_CLOSED()); } - return this.#closePromisified(); + return this.#closePromise(); } validateFunction(callback, 'callback'); + if (fsClosedir.hasSubscribers) { + const context = { api: 'callback', args: [callback], path: this.#path }; + return fsClosedir.traceCallback(this.#closeImpl, -1, context, this, + callback); + } + this.#closeImpl(callback); + } + + #closePromise() { + if (fsClosedir.hasSubscribers) { + const context = { api: 'promise', args: [], path: this.#path }; + return fsClosedir.tracePromise(this.#closePromisified, context); + } + return this.#closePromisified(); + } + + #closeImpl(callback) { if (this.#closed === true) { process.nextTick(callback, new ERR_DIR_CLOSED()); return; @@ -258,7 +281,7 @@ class Dir { if (this.#operationQueue !== null) { ArrayPrototypePush(this.#operationQueue, () => { - this.close(callback); + this.#closeImpl(callback); }); return; } @@ -289,6 +312,11 @@ class Dir { } this.#closed = true; + if (fsClosedir.hasSubscribers) { + const context = { api: 'sync', args: [], path: this.#path }; + fsClosedir.traceSync(this.#handle.close, context, this.#handle); + return; + } this.#handle.close(); } @@ -302,7 +330,7 @@ class Dir { yield result; } } finally { - await this.#closePromisified(); + await this.#closePromise(); } } @@ -313,7 +341,7 @@ class Dir { async [SymbolAsyncDispose]() { if (this.#closed) return; - await this.#closePromisified(); + await this.#closePromise(); } } @@ -328,6 +356,10 @@ ObjectDefineProperties(Dir.prototype, { }); function opendir(path, options, callback) { + if (this !== kTraced && fsOpendir.hasSubscribers) { + const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; + return traceCallback(fsOpendir, context, opendir, arguments); + } callback = typeof options === 'function' ? options : callback; validateFunction(callback, 'callback'); @@ -369,6 +401,10 @@ function opendir(path, options, callback) { } function opendirSync(path, options) { + if (this !== kTraced && fsOpendir.hasSubscribers) { + const context = { api: 'sync', args: ArrayPrototypeSlice(arguments), path }; + return traceSync(fsOpendir, context, opendirSync, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.opendirSync(path, options); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 47574a5a4711..66847cb9ef86 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -2,9 +2,11 @@ const { ArrayPrototypePush, + ArrayPrototypeSlice, Error, ErrorCaptureStackTrace, FunctionPrototypeBind, + FunctionPrototypeCall, MathMax, MathMin, Promise, @@ -88,8 +90,52 @@ const { validateStringAfterArrayBufferView, vfsState, warnOnNonPortableTemplate, + fsAccess, + fsAppendFile, + fsChmod, + fsChown, + fsClose, + fsCopyFile, + fsCp, + fsFchmod, + fsFchown, + fsFdatasync, + fsFstat, + fsFsync, + fsFtruncate, + fsFutimes, + fsLchmod, + fsLchown, + fsLink, + fsLstat, + fsLutimes, + fsMkdir, + fsMkdtemp, + fsMkstemp, + fsOpen, + fsOpendir, + fsRead, + fsReadFile, + fsReaddir, + fsReadlink, + fsReadv, + fsRealpath, + fsRename, + fsRm, + fsRmdir, + fsStat, + fsStatfs, + fsSymlink, + fsTruncate, + fsUnlink, + fsUtimes, + fsWrite, + fsWriteFile, + fsWritev, + kTraced, + tracePromise, } = require('internal/fs/utils'); -const { opendir } = require('internal/fs/dir'); +const { opendir: opendirCallback } = require('internal/fs/dir'); const { parseFileMode, validateAbortSignal, @@ -187,6 +233,14 @@ function handleErrorFromBinding(error) { return PromiseReject(error); } +function closeFileHandle(handle, fd) { + if (fsClose.hasSubscribers) { + const context = { api: 'promise', args: [], fd }; + return fsClose.tracePromise(handle.close, context, handle); + } + return handle.close(); +} + class FileHandle extends EventEmitter { #brandCheck = undefined; @@ -289,9 +343,10 @@ class FileHandle extends EventEmitter { this[kRefs]--; if (this[kRefs] === 0) { + const fd = this[kFd]; this[kFd] = -1; this[kClosePromise] = SafePromisePrototypeFinally( - this[kHandle].close(), + closeFileHandle(this[kHandle], fd), () => { this[kClosePromise] = undefined; }, ); } else { @@ -316,8 +371,14 @@ class FileHandle extends EventEmitter { if (this[kClosePromise]) { throw new ERR_INVALID_STATE('The FileHandle is closing'); } + const fd = this[kFd]; this[kFd] = -1; - this[kHandle].closeSync(); + if (fsClose.hasSubscribers) { + const context = { api: 'sync', args: [], fd }; + fsClose.traceSync(this[kHandle].closeSync, context, this[kHandle]); + } else { + this[kHandle].closeSync(); + } this.emit('close'); } @@ -466,9 +527,10 @@ class FileHandle extends EventEmitter { [kUnref]() { this[kRefs]--; if (this[kRefs] === 0) { + const fd = this[kFd]; this[kFd] = -1; PromisePrototypeThen( - this[kHandle].close(), + closeFileHandle(this[kHandle], fd), this[kCloseResolve], this[kCloseReject], ); @@ -1416,6 +1478,10 @@ async function readFileHandle(filehandle, options, knownRegularFileSize) { // All of the functions are defined as async in order to ensure that errors // thrown cause promise rejections rather than being thrown synchronously. async function access(path, mode = F_OK) { + if (this !== kTraced && fsAccess.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsAccess, context, access, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.access(path, mode); @@ -1429,6 +1495,10 @@ async function access(path, mode = F_OK) { } async function cp(src, dest, options) { + if (this !== kTraced && fsCp.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return await tracePromise(fsCp, context, cp, arguments); + } options = validateCpOptions(options); src = getValidatedPath(src, 'src'); dest = getValidatedPath(dest, 'dest'); @@ -1436,6 +1506,10 @@ async function cp(src, dest, options) { } async function copyFile(src, dest, mode) { + if (this !== kTraced && fsCopyFile.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path: src, dest }; + return await tracePromise(fsCopyFile, context, copyFile, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.copyFile(src, dest, mode); @@ -1456,6 +1530,10 @@ async function copyFile(src, dest, mode) { // Note that unlike fs.open() which uses numeric file descriptors, // fsPromises.open() uses the fs.FileHandle class. async function open(path, flags, mode) { + if (this !== kTraced && fsOpen.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsOpen, context, open, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.promisesOpen(path, flags, mode); @@ -1472,6 +1550,10 @@ async function open(path, flags, mode) { } async function read(handle, bufferOrParams, offset, length, position) { + if (this !== kTraced && fsRead.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd }; + return await tracePromise(fsRead, context, read, arguments); + } let buffer = bufferOrParams; if (!isArrayBufferView(buffer)) { // This is fh.read(params) @@ -1532,6 +1614,10 @@ async function read(handle, bufferOrParams, offset, length, position) { } async function readv(handle, buffers, position) { + if (this !== kTraced && fsReadv.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, buffers }; + return await tracePromise(fsReadv, context, readv, arguments); + } validateBufferArray(buffers); if (typeof position !== 'number') @@ -1546,6 +1632,10 @@ async function readv(handle, buffers, position) { } async function write(handle, buffer, offsetOrOptions, length, position) { + if (this !== kTraced && fsWrite.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, buffer }; + return await tracePromise(fsWrite, context, write, arguments); + } if (buffer?.byteLength === 0) return { __proto__: null, bytesWritten: 0, buffer }; @@ -1590,6 +1680,10 @@ async function write(handle, buffer, offsetOrOptions, length, position) { } async function writev(handle, buffers, position) { + if (this !== kTraced && fsWritev.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, buffers }; + return await tracePromise(fsWritev, context, writev, arguments); + } validateBufferArray(buffers); if (typeof position !== 'number') @@ -1608,6 +1702,10 @@ async function writev(handle, buffers, position) { } async function rename(oldPath, newPath) { + if (this !== kTraced && fsRename.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path: oldPath, dest: newPath }; + return await tracePromise(fsRename, context, rename, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.rename(oldPath, newPath); @@ -1623,6 +1721,10 @@ async function rename(oldPath, newPath) { } async function truncate(path, len = 0) { + if (this !== kTraced && fsTruncate.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsTruncate, context, truncate, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.truncate(path, len); @@ -1633,6 +1735,10 @@ async function truncate(path, len = 0) { } async function ftruncate(handle, len = 0) { + if (this !== kTraced && fsFtruncate.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd }; + return await tracePromise(fsFtruncate, context, ftruncate, arguments); + } validateInteger(len, 'len'); len = MathMax(0, len); return await PromisePrototypeThen( @@ -1643,6 +1749,10 @@ async function ftruncate(handle, len = 0) { } async function rm(path, options) { + if (this !== kTraced && fsRm.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsRm, context, rm, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.rm(path, options); @@ -1654,6 +1764,10 @@ async function rm(path, options) { } async function rmdir(path, options) { + if (this !== kTraced && fsRmdir.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsRmdir, context, rmdir, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.rmdir(path); @@ -1679,6 +1793,10 @@ async function rmdir(path, options) { } async function fdatasync(handle) { + if (this !== kTraced && fsFdatasync.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd }; + return await tracePromise(fsFdatasync, context, fdatasync, arguments); + } if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'); } @@ -1690,6 +1808,10 @@ async function fdatasync(handle) { } async function fsync(handle) { + if (this !== kTraced && fsFsync.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd }; + return await tracePromise(fsFsync, context, fsync, arguments); + } if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'); } @@ -1701,6 +1823,10 @@ async function fsync(handle) { } async function mkdir(path, options) { + if (this !== kTraced && fsMkdir.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsMkdir, context, mkdir, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.mkdir(path, options); @@ -1768,6 +1894,10 @@ async function readdirRecursiveWithPermissionModel(basePath, options) { } async function readdir(path, options) { + if (this !== kTraced && fsReaddir.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsReaddir, context, readdir, arguments); + } options = getOptions(options); // Make shallow copy to prevent mutating options from affecting results @@ -1799,6 +1929,10 @@ async function readdir(path, options) { } async function readlink(path, options) { + if (this !== kTraced && fsReadlink.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsReadlink, context, readlink, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.readlink(path, options); @@ -1814,6 +1948,10 @@ async function readlink(path, options) { } async function symlink(target, path, type) { + if (this !== kTraced && fsSymlink.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), target, path }; + return await tracePromise(fsSymlink, context, symlink, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.symlink(target, path, type); @@ -1853,6 +1991,10 @@ async function symlink(target, path, type) { } async function fstat(handle, options = { __proto__: null, bigint: false }) { + if (this !== kTraced && fsFstat.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd }; + return await tracePromise(fsFstat, context, fstat, arguments); + } validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1869,6 +2011,10 @@ async function fstat(handle, options = { __proto__: null, bigint: false }) { } async function lstat(path, options = { __proto__: null, bigint: false }) { + if (this !== kTraced && fsLstat.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsLstat, context, lstat, arguments); + } validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1896,6 +2042,10 @@ async function lstat(path, options = { __proto__: null, bigint: false }) { } async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (this !== kTraced && fsStat.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsStat, context, stat, arguments); + } validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1921,6 +2071,10 @@ async function stat(path, options = { __proto__: null, bigint: false, throwIfNoE } async function statfs(path, options = { __proto__: null, bigint: false }) { + if (this !== kTraced && fsStatfs.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsStatfs, context, statfs, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.statfs(path, options); @@ -1936,6 +2090,10 @@ async function statfs(path, options = { __proto__: null, bigint: false }) { } async function link(existingPath, newPath) { + if (this !== kTraced && fsLink.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path: existingPath, dest: newPath }; + return await tracePromise(fsLink, context, link, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.link(existingPath, newPath); @@ -1951,6 +2109,10 @@ async function link(existingPath, newPath) { } async function unlink(path) { + if (this !== kTraced && fsUnlink.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsUnlink, context, unlink, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.unlink(path); @@ -1964,6 +2126,10 @@ async function unlink(path) { } async function fchmod(handle, mode) { + if (this !== kTraced && fsFchmod.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, mode }; + return await tracePromise(fsFchmod, context, fchmod, arguments); + } if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'); } @@ -1976,6 +2142,10 @@ async function fchmod(handle, mode) { } async function chmod(path, mode) { + if (this !== kTraced && fsChmod.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, mode }; + return await tracePromise(fsChmod, context, chmod, arguments); + } path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); @@ -1993,6 +2163,10 @@ async function chmod(path, mode) { } async function lchmod(path, mode) { + if (this !== kTraced && fsLchmod.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, mode }; + return await tracePromise(fsLchmod, context, lchmod, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.lchmod(path, mode); @@ -2007,6 +2181,10 @@ async function lchmod(path, mode) { } async function lchown(path, uid, gid) { + if (this !== kTraced && fsLchown.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return await tracePromise(fsLchown, context, lchown, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.lchown(path, uid, gid); @@ -2024,6 +2202,10 @@ async function lchown(path, uid, gid) { } async function fchown(handle, uid, gid) { + if (this !== kTraced && fsFchown.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, uid, gid }; + return await tracePromise(fsFchown, context, fchown, arguments); + } validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); if (permission.isEnabled()) { @@ -2037,6 +2219,10 @@ async function fchown(handle, uid, gid) { } async function chown(path, uid, gid) { + if (this !== kTraced && fsChown.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, uid, gid }; + return await tracePromise(fsChown, context, chown, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.chown(path, uid, gid); @@ -2054,6 +2240,10 @@ async function chown(path, uid, gid) { } async function utimes(path, atime, mtime) { + if (this !== kTraced && fsUtimes.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return await tracePromise(fsUtimes, context, utimes, arguments); + } path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2072,6 +2262,10 @@ async function utimes(path, atime, mtime) { } async function futimes(handle, atime, mtime) { + if (this !== kTraced && fsFutimes.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments, 1), fd: handle.fd, atime, mtime }; + return await tracePromise(fsFutimes, context, futimes, arguments); + } if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'); } @@ -2085,6 +2279,10 @@ async function futimes(handle, atime, mtime) { } async function lutimes(path, atime, mtime) { + if (this !== kTraced && fsLutimes.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, atime, mtime }; + return await tracePromise(fsLutimes, context, lutimes, arguments); + } path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2103,6 +2301,10 @@ async function lutimes(path, atime, mtime) { } async function realpath(path, options) { + if (this !== kTraced && fsRealpath.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsRealpath, context, realpath, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.realpath(path, options); @@ -2117,6 +2319,10 @@ async function realpath(path, options) { } async function mkdtemp(prefix, options) { + if (this !== kTraced && fsMkdtemp.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), prefix }; + return await tracePromise(fsMkdtemp, context, mkdtemp, arguments); + } const h = vfsState.handlers; if (h !== null) { const promise = h.mkdtemp(prefix, options); @@ -2137,6 +2343,10 @@ async function mkdtemp(prefix, options) { } async function mkstemp(prefix, options) { + if (this !== kTraced && fsMkstemp.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), prefix }; + return await tracePromise(fsMkstemp, context, mkstemp, arguments); + } options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -2159,6 +2369,10 @@ async function mkstemp(prefix, options) { } async function mkdtempDisposable(prefix, options) { + if (this !== kTraced && fsMkdtemp.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), prefix }; + return await tracePromise(fsMkdtemp, context, mkdtempDisposable, arguments); + } options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -2194,6 +2408,10 @@ async function mkdtempDisposable(prefix, options) { } async function writeFile(path, data, options) { + if (this !== kTraced && fsWriteFile.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, data }; + return await tracePromise(fsWriteFile, context, writeFile, arguments); + } options = getOptions(options, { encoding: 'utf8', mode: 0o666, @@ -2274,6 +2492,10 @@ function isCustomIterable(obj) { } async function appendFile(path, data, options) { + if (this !== kTraced && fsAppendFile.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path, data }; + return await tracePromise(fsAppendFile, context, appendFile, arguments); + } options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); parseFileMode(options.mode, 'mode', 0o666); @@ -2289,6 +2511,10 @@ async function appendFile(path, data, options) { } async function readFile(path, options) { + if (this !== kTraced && fsReadFile.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return await tracePromise(fsReadFile, context, readFile, arguments); + } const h = vfsState.handlers; if (h !== null) { checkAborted(options?.signal); @@ -2347,6 +2573,17 @@ function readFileInOneRoundTrip(path, flagsNumber) { }); } +const opendirPromisified = promisify(opendirCallback); +// The unused `callback` keeps the length of promisify(fs.opendir). +function opendir(path, options, callback) { + if (this !== kTraced && fsOpendir.hasSubscribers) { + const context = { api: 'promise', args: ArrayPrototypeSlice(arguments), path }; + return tracePromise(fsOpendir, context, opendir, arguments); + } + // Skips the check in fs.opendir(), so the call is traced only once. + return FunctionPrototypeCall(opendirPromisified, kTraced, path, options); +} + async function* _watch(filename, options = kEmptyObject) { const h = vfsState.handlers; if (h !== null) { @@ -2388,7 +2625,7 @@ module.exports = { cp, glob, open, - opendir: promisify(opendir), + opendir, rename, truncate, rm, diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index 29f254d92501..272d3ff9d930 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -3,6 +3,7 @@ const { ArrayIsArray, ArrayPrototypePush, + ArrayPrototypePushApply, BigInt, Date, DateNow, @@ -17,6 +18,7 @@ const { ObjectDefineProperties, ObjectIs, ObjectSetPrototypeOf, + ReflectApply, ReflectOwnKeys, RegExpPrototypeSymbolReplace, StringPrototypeEndsWith, @@ -78,6 +80,7 @@ const kPartialMtimeNs = Symbol('partialMtimeNs'); const kPartialCtimeNs = Symbol('partialCtimeNs'); const kPartialBirthtimeNs = Symbol('kPartialBirthtimeNs'); const assert = require('internal/assert'); +const { tracingChannel } = require('diagnostics_channel'); const { fs: { @@ -1193,7 +1196,140 @@ const validatePosition = hideStackFrames((position, name, length) => { const vfsState = { __proto__: null, handlers: null }; function setVfsHandlers(handlers) { vfsState.handlers = handlers; } +// Each traced fs function starts with: +// +// if (this !== kTraced && fsStat.hasSubscribers) { +// const context = { api: 'callback', args: ArrayPrototypeSlice(arguments), path }; +// return traceCallback(fsStat, context, stat, arguments); +// } +// +// The trace helpers call the same function again inside the trace, so +// that its whole body runs inside the `start` scope. They pass kTraced +// as `this`, so that the second entry skips the check. No traced +// function uses `this` for anything else. +const kTraced = Symbol('kTraced'); + +function applyTraced(fn, args) { + return ReflectApply(fn, kTraced, args); +} + +function applyTracedSpread(fn, ...args) { + return ReflectApply(fn, kTraced, args); +} + +function traceSync(channel, context, fn, args) { + return channel.traceSync(applyTraced, context, undefined, fn, args); +} + +function tracePromise(channel, context, fn, args) { + return channel.tracePromise(applyTraced, context, undefined, fn, args); +} + +// The callback is the last function argument. Without one, the function +// is called again untraced, because it throws for a missing callback. +function traceCallback(channel, context, fn, args) { + let position = args.length - 1; + while (position >= 0 && typeof args[position] !== 'function') position--; + if (position < 0) return applyTraced(fn, args); + const callArgs = [applyTracedSpread, position + 1, context, undefined, fn]; + ArrayPrototypePushApply(callArgs, args); + return ReflectApply(channel.traceCallback, channel, callArgs); +} + +const fsAccess = tracingChannel('fs.access'); +const fsAppendFile = tracingChannel('fs.appendFile'); +const fsChmod = tracingChannel('fs.chmod'); +const fsChown = tracingChannel('fs.chown'); +const fsClose = tracingChannel('fs.close'); +const fsClosedir = tracingChannel('fs.closedir'); +const fsCopyFile = tracingChannel('fs.copyFile'); +const fsCp = tracingChannel('fs.cp'); +const fsExists = tracingChannel('fs.exists'); +const fsFchmod = tracingChannel('fs.fchmod'); +const fsFchown = tracingChannel('fs.fchown'); +const fsFdatasync = tracingChannel('fs.fdatasync'); +const fsFstat = tracingChannel('fs.fstat'); +const fsFsync = tracingChannel('fs.fsync'); +const fsFtruncate = tracingChannel('fs.ftruncate'); +const fsFutimes = tracingChannel('fs.futimes'); +const fsGlob = tracingChannel('fs.glob'); +const fsLchmod = tracingChannel('fs.lchmod'); +const fsLchown = tracingChannel('fs.lchown'); +const fsLink = tracingChannel('fs.link'); +const fsLstat = tracingChannel('fs.lstat'); +const fsLutimes = tracingChannel('fs.lutimes'); +const fsMkdir = tracingChannel('fs.mkdir'); +const fsMkdtemp = tracingChannel('fs.mkdtemp'); +const fsMkstemp = tracingChannel('fs.mkstemp'); +const fsOpen = tracingChannel('fs.open'); +const fsOpenAsBlob = tracingChannel('fs.openAsBlob'); +const fsOpendir = tracingChannel('fs.opendir'); +const fsRead = tracingChannel('fs.read'); +const fsReadFile = tracingChannel('fs.readFile'); +const fsReaddir = tracingChannel('fs.readdir'); +const fsReadlink = tracingChannel('fs.readlink'); +const fsReadv = tracingChannel('fs.readv'); +const fsRealpath = tracingChannel('fs.realpath'); +const fsRename = tracingChannel('fs.rename'); +const fsRm = tracingChannel('fs.rm'); +const fsRmdir = tracingChannel('fs.rmdir'); +const fsStat = tracingChannel('fs.stat'); +const fsStatfs = tracingChannel('fs.statfs'); +const fsSymlink = tracingChannel('fs.symlink'); +const fsTruncate = tracingChannel('fs.truncate'); +const fsUnlink = tracingChannel('fs.unlink'); +const fsUtimes = tracingChannel('fs.utimes'); +const fsWrite = tracingChannel('fs.write'); +const fsWriteFile = tracingChannel('fs.writeFile'); +const fsWritev = tracingChannel('fs.writev'); + module.exports = { + fsAccess, + fsAppendFile, + fsChmod, + fsChown, + fsClose, + fsClosedir, + fsCopyFile, + fsCp, + fsExists, + fsFchmod, + fsFchown, + fsFdatasync, + fsFstat, + fsFsync, + fsFtruncate, + fsFutimes, + fsGlob, + fsLchmod, + fsLchown, + fsLink, + fsLstat, + fsLutimes, + fsMkdir, + fsMkdtemp, + fsMkstemp, + fsOpen, + fsOpenAsBlob, + fsOpendir, + fsRead, + fsReadFile, + fsReaddir, + fsReadlink, + fsReadv, + fsRealpath, + fsRename, + fsRm, + fsRmdir, + fsStat, + fsStatfs, + fsSymlink, + fsTruncate, + fsUnlink, + fsUtimes, + fsWrite, + fsWriteFile, + fsWritev, collectRecursiveReaddirResult, constants: { kIoMaxLength, @@ -1216,6 +1352,10 @@ module.exports = { getValidatedFd, getValidatedPath, handleErrorFromBinding, + kTraced, + traceCallback, + tracePromise, + traceSync, preprocessSymlinkDestination, realpathCacheKey: Symbol('realpathCacheKey'), getStatFsFromBinding, diff --git a/test/parallel/test-diagnostics-channel-fs-bind-store.js b/test/parallel/test-diagnostics-channel-fs-bind-store.js new file mode 100644 index 000000000000..f272b69eded9 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-bind-store.js @@ -0,0 +1,96 @@ +'use strict'; + +// Checks that stores bound to the fs tracing channels reach the async +// work, the user callback and nested operations. + +const common = require('../common'); +const assert = require('node:assert'); +const { AsyncLocalStorage } = require('node:async_hooks'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const file = tmpdir.resolve('file.txt'); +fs.writeFileSync(file, 'hello'); + +const startStore = new AsyncLocalStorage(); +const asyncStartStore = new AsyncLocalStorage(); + +for (const name of ['stat', 'readFile', 'truncate']) { + const channel = dc.tracingChannel(`fs.${name}`); + channel.start.bindStore(startStore, (context) => ({ name, context })); + channel.asyncStart.bindStore(asyncStartStore, (context) => ({ name, context })); +} + +// The store that is active when a nested `fs.open` starts. +const openStores = []; +dc.tracingChannel('fs.open').subscribe({ + start() { openStores.push(startStore.getStore()); }, +}); + +function testCallback() { + startStore.run('outer', common.mustCall(() => { + fs.stat(file, common.mustSucceed(() => { + assert.strictEqual(startStore.getStore().name, 'stat'); + assert.strictEqual(asyncStartStore.getStore().name, 'stat'); + assert.strictEqual(startStore.getStore().context, + asyncStartStore.getStore().context); + process.nextTick(testCallbackNested); + })); + assert.strictEqual(startStore.getStore(), 'outer'); + })); +} + +function testCallbackNested() { + openStores.length = 0; + startStore.run('outer', common.mustCall(() => { + fs.truncate(file, 5, common.mustSucceed(() => { + assert.strictEqual(openStores.length, 1); + assert.strictEqual(openStores[0].name, 'truncate'); + process.nextTick(testSync); + })); + })); +} + +function testSync() { + openStores.length = 0; + startStore.run('outer', common.mustCall(() => { + // Without an encoding, readFileSync() opens the file with + // fs.openSync(), which runs inside the readFile scope. + fs.readFileSync(file); + assert.strictEqual(openStores.length, 1); + assert.strictEqual(openStores[0].name, 'readFile'); + assert.strictEqual(startStore.getStore(), 'outer'); + })); + testPromise().then(common.mustCall()); +} + +async function testPromise() { + let asyncStartContext; + const onAsyncStart = common.mustCall(() => { + asyncStartContext = startStore.getStore(); + }); + const stat = dc.tracingChannel('fs.stat'); + stat.asyncStart.subscribe(onAsyncStart); + + await startStore.run('outer', common.mustCall(async () => { + await fsp.stat(file); + // The caller's continuation keeps its own store. + assert.strictEqual(startStore.getStore(), 'outer'); + })); + stat.asyncStart.unsubscribe(onAsyncStart); + assert.strictEqual(asyncStartContext.name, 'stat'); + + openStores.length = 0; + await startStore.run('outer', common.mustCall(async () => { + await fsp.truncate(file, 2); + assert.strictEqual(startStore.getStore(), 'outer'); + })); + assert.strictEqual(openStores.length, 1); + assert.strictEqual(openStores[0].name, 'truncate'); +} + +testCallback(); diff --git a/test/parallel/test-diagnostics-channel-fs-callback.js b/test/parallel/test-diagnostics-channel-fs-callback.js new file mode 100644 index 000000000000..6ec5b592c931 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-callback.js @@ -0,0 +1,189 @@ +'use strict'; + +// Checks the events that callback fs functions publish, and that the +// user callback runs between asyncStart and asyncEnd. + +const common = require('../common'); +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const file = tmpdir.resolve('file.txt'); +const missing = tmpdir.resolve('missing.txt'); +fs.writeFileSync(file, 'hello'); + +function record(name, log) { + const channel = dc.tracingChannel(`fs.${name}`); + const handlers = {}; + for (const type of ['start', 'end', 'asyncStart', 'asyncEnd', 'error']) { + handlers[type] = (context) => log.push({ type, context }); + } + channel.subscribe(handlers); + return () => channel.unsubscribe(handlers); +} + +function types(log) { + return log.map((e) => e.type); +} + +function assertSameContext(log) { + const contexts = log.filter((e) => e.context).map((e) => e.context); + for (const context of contexts) { + assert.strictEqual(context, contexts[0]); + } +} + +function testStat() { + const log = []; + const stop = record('stat', log); + fs.stat(file, common.mustSucceed((stats) => { + log.push({ type: 'callback' }); + process.nextTick(() => { + stop(); + assert.deepStrictEqual(types(log), + ['start', 'end', 'asyncStart', 'callback', 'asyncEnd']); + assertSameContext(log); + const { context } = log[0]; + assert.strictEqual(context.api, 'callback'); + assert.strictEqual(context.path, file); + assert.strictEqual(context.result, stats); + assert.ok(stats instanceof fs.Stats); + testStatError(); + }); + })); + // The callback has not run yet. + assert.deepStrictEqual(types(log), ['start', 'end']); +} + +function testStatError() { + const log = []; + const stop = record('stat', log); + fs.stat(missing, common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + log.push({ type: 'callback' }); + process.nextTick(() => { + stop(); + assert.deepStrictEqual( + types(log), + ['start', 'end', 'error', 'asyncStart', 'callback', 'asyncEnd']); + assertSameContext(log); + assert.strictEqual(log[0].context.error, err); + testRead(); + }); + })); +} + +function testRead() { + const log = []; + const stop = record('read', log); + const fd = fs.openSync(file, 'r'); + const buffer = Buffer.alloc(5); + fs.read(fd, buffer, 0, 5, 0, common.mustSucceed((bytesRead) => { + log.push({ type: 'callback' }); + process.nextTick(() => { + stop(); + fs.closeSync(fd); + assert.deepStrictEqual(types(log), + ['start', 'end', 'asyncStart', 'callback', 'asyncEnd']); + const { context } = log[0]; + assert.strictEqual(context.fd, fd); + assert.strictEqual(context.args[1], buffer); + assert.strictEqual(context.result, bytesRead); + assert.strictEqual(bytesRead, 5); + testRename(); + }); + })); +} + +function testRename() { + const log = []; + const stop = record('rename', log); + const dest = tmpdir.resolve('renamed.txt'); + fs.rename(file, dest, common.mustSucceed(() => { + process.nextTick(() => { + stop(); + assert.deepStrictEqual(types(log), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(log[0].context.path, file); + assert.strictEqual(log[0].context.dest, dest); + fs.renameSync(dest, file); + testExists(); + }); + })); +} + +function testExists() { + const log = []; + const stop = record('exists', log); + const callback = common.mustCall((exists) => { + assert.strictEqual(exists, true); + process.nextTick(() => { + stop(); + assert.deepStrictEqual(types(log), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(log[0].context.result, true); + assert.deepStrictEqual(log[0].context.args, [file, callback]); + testReadFile(); + }); + }); + fs.exists(file, callback); +} + +function testReadFile() { + const log = []; + const stop = record('readFile', log); + fs.readFile(file, 'utf8', common.mustSucceed((data) => { + process.nextTick(() => { + stop(); + assert.deepStrictEqual(types(log), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(log[0].context.path, file); + assert.strictEqual(log[0].context.result, data); + assert.strictEqual(data, 'hello'); + testRealpathNativeReplaced(); + }); + })); +} + +function testRealpathNativeReplaced() { + // A replaced `realpath.native` runs once per traced call, because the + // trace calls the original function, not the property. + const original = fs.realpath.native; + let calls = 0; + fs.realpath.native = function(path, options, callback) { + calls++; + return original(path, options, callback); + }; + const log = []; + const stop = record('realpath', log); + fs.realpath.native(file, common.mustSucceed((resolved) => { + // An exception thrown in this callback, or in a nextTick from it, is + // lost (a bug in realpath.native), so the checks run from setImmediate. + setImmediate(common.mustCall(() => { + stop(); + fs.realpath.native = original; + assert.strictEqual(resolved, fs.realpathSync(file)); + assert.strictEqual(calls, 1); + assert.deepStrictEqual(types(log), ['start', 'end', 'asyncStart', 'asyncEnd']); + testCloseWithoutCallback(); + })); + })); +} + +function testCloseWithoutCallback() { + const fd = fs.openSync(file, 'r'); + const channel = dc.tracingChannel('fs.close'); + const handlers = { + start: common.mustCall((context) => { + assert.strictEqual(context.api, 'callback'); + assert.deepStrictEqual(context.args, [fd]); + }), + asyncEnd: common.mustCall(() => { + process.nextTick(() => channel.unsubscribe(handlers)); + }), + }; + channel.subscribe(handlers); + fs.close(fd); +} + +testStat(); diff --git a/test/parallel/test-diagnostics-channel-fs-coverage.js b/test/parallel/test-diagnostics-channel-fs-coverage.js new file mode 100644 index 000000000000..1e38f245b49d --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-coverage.js @@ -0,0 +1,336 @@ +'use strict'; + +// Checks that every public fs operation publishes to its tracing +// channel, in all of its forms. A new public function must be added to +// the tables below, or to the list of functions that do not publish. + +const common = require('../common'); +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const path = require('node:path'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const file = tmpdir.resolve('file.txt'); +const now = new Date(); +const { uid, gid } = (() => { + fs.writeFileSync(file, 'hello world'); + return fs.statSync(file); +})(); + +let counter = 0; +function fresh(name) { + return tmpdir.resolve(`${name}-${counter++}`); +} + +function newFile() { + const target = fresh('file'); + fs.writeFileSync(target, 'hello world'); + return target; +} + +function newEmptyDir() { + const target = fresh('empty'); + fs.mkdirSync(target); + return target; +} + +function newDir() { + const target = fresh('dir'); + fs.mkdirSync(target); + fs.writeFileSync(path.join(target, 'a.txt'), 'a'); + return target; +} + +function newLink() { + const target = fresh('link'); + try { + fs.symlinkSync(file, target); + } catch { + // Symbolic links can need extra privileges on Windows. The + // operation then fails, which still publishes events. + } + return target; +} + +// Not file system operations, or covered by the operations they use. +const notTraced = new Set([ + 'Dir', 'Dirent', 'FileReadStream', 'FileWriteStream', 'ReadStream', + 'Stats', 'Utf8Stream', 'WriteStream', '_toUnixTimestamp', + 'createReadStream', 'createWriteStream', 'unwatchFile', 'watch', + 'watchFile', +]); +const notTracedPromises = new Set(['constants', 'glob', 'watch']); + +// [export name, channel name, call] +const syncOps = [ + ['accessSync', 'access', () => fs.accessSync(file)], + ['appendFileSync', 'appendFile', () => fs.appendFileSync(newFile(), 'x')], + ['chmodSync', 'chmod', () => fs.chmodSync(newFile(), 0o644)], + ['chownSync', 'chown', () => fs.chownSync(newFile(), uid, gid)], + ['closeSync', 'close', () => fs.closeSync(fs.openSync(file))], + ['copyFileSync', 'copyFile', () => fs.copyFileSync(file, fresh('copy'))], + ['cpSync', 'cp', () => fs.cpSync(file, fresh('cp'))], + ['existsSync', 'exists', () => fs.existsSync(file)], + ['fchmodSync', 'fchmod', (fd) => fs.fchmodSync(fd, 0o644)], + ['fchownSync', 'fchown', (fd) => fs.fchownSync(fd, uid, gid)], + ['fdatasyncSync', 'fdatasync', (fd) => fs.fdatasyncSync(fd)], + ['fstatSync', 'fstat', (fd) => fs.fstatSync(fd)], + ['fsyncSync', 'fsync', (fd) => fs.fsyncSync(fd)], + ['ftruncateSync', 'ftruncate', (fd) => fs.ftruncateSync(fd, 11)], + ['futimesSync', 'futimes', (fd) => fs.futimesSync(fd, now, now)], + ['globSync', 'glob', () => fs.globSync('*', { cwd: newDir() })], + ['lchmodSync', 'lchmod', () => fs.lchmodSync(newLink(), 0o644)], + ['lchownSync', 'lchown', () => fs.lchownSync(newLink(), uid, gid)], + ['linkSync', 'link', () => fs.linkSync(file, fresh('hardlink'))], + ['lstatSync', 'lstat', () => fs.lstatSync(file)], + ['lutimesSync', 'lutimes', () => fs.lutimesSync(newLink(), now, now)], + ['mkdirSync', 'mkdir', () => fs.mkdirSync(fresh('mkdir'))], + ['mkdtempSync', 'mkdtemp', () => fs.mkdtempSync(fresh('tmp'))], + ['mkdtempDisposableSync', 'mkdtemp', + () => fs.mkdtempDisposableSync(fresh('tmp')).remove()], + ['mkstempSync', 'mkstemp', () => fs.closeSync(fs.mkstempSync(fresh('tmp')).fd)], + ['openAsBlobSync', 'openAsBlob', () => fs.openAsBlobSync(file)], + ['openSync', 'open', () => fs.closeSync(fs.openSync(file))], + ['opendirSync', 'opendir', () => fs.opendirSync(newDir()).closeSync()], + ['readFileSync', 'readFile', () => fs.readFileSync(file)], + ['readSync', 'read', (fd) => fs.readSync(fd, Buffer.alloc(4), 0, 4, 0)], + ['readdirSync', 'readdir', () => fs.readdirSync(newDir())], + ['readlinkSync', 'readlink', () => fs.readlinkSync(newLink())], + ['readvSync', 'readv', (fd) => fs.readvSync(fd, [Buffer.alloc(4)], 0)], + ['realpathSync', 'realpath', () => fs.realpathSync(file)], + ['realpathSync.native', 'realpath', () => fs.realpathSync.native(file)], + ['renameSync', 'rename', () => fs.renameSync(newFile(), fresh('renamed'))], + ['rmSync', 'rm', () => fs.rmSync(newDir(), { recursive: true })], + ['rmdirSync', 'rmdir', () => fs.rmdirSync(newEmptyDir())], + ['statSync', 'stat', () => fs.statSync(file)], + ['statfsSync', 'statfs', () => fs.statfsSync(file)], + ['symlinkSync', 'symlink', () => fs.symlinkSync(file, fresh('symlink'))], + ['truncateSync', 'truncate', () => fs.truncateSync(newFile(), 2)], + ['unlinkSync', 'unlink', () => fs.unlinkSync(newFile())], + ['utimesSync', 'utimes', () => fs.utimesSync(file, now, now)], + ['writeFileSync', 'writeFile', () => fs.writeFileSync(fresh('written'), 'x')], + ['writeSync', 'write', (fd) => fs.writeSync(fd, Buffer.from('x'))], + ['writevSync', 'writev', (fd) => fs.writevSync(fd, [Buffer.from('x')])], + ['Dir.closeSync', 'closedir', (fd, dir) => dir.closeSync()], +]; + +const callbackOps = [ + ['access', 'access', (cb) => fs.access(file, cb)], + ['appendFile', 'appendFile', (cb) => fs.appendFile(newFile(), 'x', cb)], + ['chmod', 'chmod', (cb) => fs.chmod(newFile(), 0o644, cb)], + ['chown', 'chown', (cb) => fs.chown(newFile(), uid, gid, cb)], + ['close', 'close', (cb) => fs.close(fs.openSync(file), cb)], + ['copyFile', 'copyFile', (cb) => fs.copyFile(file, fresh('copy'), cb)], + ['cp', 'cp', (cb) => fs.cp(file, fresh('cp'), cb)], + ['exists', 'exists', (cb) => fs.exists(file, () => cb())], + ['fchmod', 'fchmod', (cb, fd) => fs.fchmod(fd, 0o644, cb)], + ['fchown', 'fchown', (cb, fd) => fs.fchown(fd, uid, gid, cb)], + ['fdatasync', 'fdatasync', (cb, fd) => fs.fdatasync(fd, cb)], + ['fstat', 'fstat', (cb, fd) => fs.fstat(fd, cb)], + ['fsync', 'fsync', (cb, fd) => fs.fsync(fd, cb)], + ['ftruncate', 'ftruncate', (cb, fd) => fs.ftruncate(fd, 11, cb)], + ['futimes', 'futimes', (cb, fd) => fs.futimes(fd, now, now, cb)], + ['glob', 'glob', (cb) => fs.glob('*', { cwd: newDir() }, cb)], + ['lchmod', 'lchmod', (cb) => fs.lchmod(newLink(), 0o644, cb)], + ['lchown', 'lchown', (cb) => fs.lchown(newLink(), uid, gid, cb)], + ['link', 'link', (cb) => fs.link(file, fresh('hardlink'), cb)], + ['lstat', 'lstat', (cb) => fs.lstat(file, cb)], + ['lutimes', 'lutimes', (cb) => fs.lutimes(newLink(), now, now, cb)], + ['mkdir', 'mkdir', (cb) => fs.mkdir(fresh('mkdir'), cb)], + ['mkdtemp', 'mkdtemp', (cb) => fs.mkdtemp(fresh('tmp'), cb)], + ['mkstemp', 'mkstemp', (cb) => fs.mkstemp(fresh('tmp'), (err, file) => { + if (file !== undefined) fs.closeSync(file.fd); + cb(err); + })], + ['open', 'open', (cb) => fs.open(file, (err, fd) => { + if (fd !== undefined) fs.closeSync(fd); + cb(err); + })], + ['opendir', 'opendir', (cb) => fs.opendir(newDir(), (err, dir) => { + dir?.closeSync(); + cb(err); + })], + ['read', 'read', (cb, fd) => fs.read(fd, Buffer.alloc(4), 0, 4, 0, cb)], + ['readFile', 'readFile', (cb) => fs.readFile(file, cb)], + ['readdir', 'readdir', (cb) => fs.readdir(newDir(), cb)], + ['readlink', 'readlink', (cb) => fs.readlink(newLink(), cb)], + ['readv', 'readv', (cb, fd) => fs.readv(fd, [Buffer.alloc(4)], 0, cb)], + ['realpath', 'realpath', (cb) => fs.realpath(file, cb)], + ['realpath.native', 'realpath', (cb) => fs.realpath.native(file, cb)], + ['rename', 'rename', (cb) => fs.rename(newFile(), fresh('renamed'), cb)], + ['rm', 'rm', (cb) => fs.rm(newDir(), { recursive: true }, cb)], + ['rmdir', 'rmdir', (cb) => fs.rmdir(newEmptyDir(), cb)], + ['stat', 'stat', (cb) => fs.stat(file, cb)], + ['statfs', 'statfs', (cb) => fs.statfs(file, cb)], + ['symlink', 'symlink', (cb) => fs.symlink(file, fresh('symlink'), cb)], + ['truncate', 'truncate', (cb) => fs.truncate(newFile(), 2, cb)], + ['unlink', 'unlink', (cb) => fs.unlink(newFile(), cb)], + ['utimes', 'utimes', (cb) => fs.utimes(file, now, now, cb)], + ['write', 'write', (cb, fd) => fs.write(fd, Buffer.from('x'), cb)], + ['writeFile', 'writeFile', (cb) => fs.writeFile(fresh('written'), 'x', cb)], + ['writev', 'writev', (cb, fd) => fs.writev(fd, [Buffer.from('x')], cb)], + ['Dir.close', 'closedir', (cb, fd, dir) => dir.close(cb)], +]; + +// FileHandle methods are listed with a `FileHandle.` prefix. +const promiseOps = [ + ['access', 'access', () => fsp.access(file)], + ['appendFile', 'appendFile', () => fsp.appendFile(newFile(), 'x')], + ['chmod', 'chmod', () => fsp.chmod(newFile(), 0o644)], + ['chown', 'chown', () => fsp.chown(newFile(), uid, gid)], + ['copyFile', 'copyFile', () => fsp.copyFile(file, fresh('copy'))], + ['cp', 'cp', () => fsp.cp(file, fresh('cp'))], + ['lchmod', 'lchmod', () => fsp.lchmod(newLink(), 0o644)], + ['lchown', 'lchown', () => fsp.lchown(newLink(), uid, gid)], + ['link', 'link', () => fsp.link(file, fresh('hardlink'))], + ['lstat', 'lstat', () => fsp.lstat(file)], + ['lutimes', 'lutimes', () => fsp.lutimes(newLink(), now, now)], + ['mkdir', 'mkdir', () => fsp.mkdir(fresh('mkdir'))], + ['mkdtemp', 'mkdtemp', () => fsp.mkdtemp(fresh('tmp'))], + ['mkdtempDisposable', 'mkdtemp', + async () => (await fsp.mkdtempDisposable(fresh('tmp'))).remove()], + ['mkstemp', 'mkstemp', async () => (await fsp.mkstemp(fresh('tmp'))).handle.close()], + ['open', 'open', async () => (await fsp.open(file)).close()], + ['opendir', 'opendir', async () => (await fsp.opendir(newDir())).close()], + ['openAsBlob', 'openAsBlob', () => fs.openAsBlob(file)], + ['readFile', 'readFile', () => fsp.readFile(file)], + ['readdir', 'readdir', () => fsp.readdir(newDir())], + ['readlink', 'readlink', () => fsp.readlink(newLink())], + ['realpath', 'realpath', () => fsp.realpath(file)], + ['rename', 'rename', () => fsp.rename(newFile(), fresh('renamed'))], + ['rm', 'rm', () => fsp.rm(newDir(), { recursive: true })], + ['rmdir', 'rmdir', () => fsp.rmdir(newEmptyDir())], + ['stat', 'stat', () => fsp.stat(file)], + ['statfs', 'statfs', () => fsp.statfs(file)], + ['symlink', 'symlink', () => fsp.symlink(file, fresh('symlink'))], + ['truncate', 'truncate', () => fsp.truncate(newFile(), 2)], + ['unlink', 'unlink', () => fsp.unlink(newFile())], + ['utimes', 'utimes', () => fsp.utimes(file, now, now)], + ['writeFile', 'writeFile', () => fsp.writeFile(fresh('written'), 'x')], + ['FileHandle.appendFile', 'writeFile', (fh) => fh.appendFile('x')], + ['FileHandle.chmod', 'fchmod', (fh) => fh.chmod(0o644)], + ['FileHandle.chown', 'fchown', (fh) => fh.chown(uid, gid)], + ['FileHandle.close', 'close', (fh) => fh.close()], + ['FileHandle.datasync', 'fdatasync', (fh) => fh.datasync()], + ['FileHandle.read', 'read', (fh) => fh.read(Buffer.alloc(4), 0, 4, 0)], + ['FileHandle.readFile', 'readFile', (fh) => fh.readFile()], + ['FileHandle.readv', 'readv', (fh) => fh.readv([Buffer.alloc(4)], 0)], + ['FileHandle.stat', 'fstat', (fh) => fh.stat()], + ['FileHandle.sync', 'fsync', (fh) => fh.sync()], + ['FileHandle.truncate', 'ftruncate', (fh) => fh.truncate(11)], + ['FileHandle.utimes', 'futimes', (fh) => fh.utimes(now, now)], + ['FileHandle.write', 'write', (fh) => fh.write(Buffer.from('x'))], + ['FileHandle.writeFile', 'writeFile', (fh) => fh.writeFile('x')], + ['FileHandle.writev', 'writev', (fh) => fh.writev([Buffer.from('x')])], + ['Dir.close', 'closedir', () => fs.opendirSync(newDir()).close()], +]; + +function supported(name) { + // The lchmod functions only exist where O_SYMLINK does. + return !name.includes('lchmod') || fs.lchmod !== undefined; +} + +function listed(ops) { + return new Set(ops.map(([name]) => name)); +} + +{ + // Every public function is listed. + const sync = listed(syncOps); + const callback = listed(callbackOps); + const promise = listed(promiseOps); + for (const [name, value] of Object.entries(fs)) { + if (typeof value !== 'function' || notTraced.has(name)) continue; + if (!supported(name)) continue; + // The fs.openAsBlob() function returns a promise. + assert.ok(sync.has(name) || callback.has(name) || promise.has(name), + `fs.${name} is not listed`); + } + for (const [name, value] of Object.entries(fsp)) { + if (typeof value !== 'function' || notTracedPromises.has(name)) continue; + if (!supported(name)) continue; + assert.ok(promise.has(name), `fs.promises.${name} is not listed`); + } +} + +function watch(channelName) { + const events = []; + const channel = dc.tracingChannel(`fs.${channelName}`); + const handlers = {}; + for (const type of ['start', 'end', 'asyncStart', 'asyncEnd', 'error']) { + handlers[type] = (context) => events.push({ type, context }); + } + channel.subscribe(handlers); + return { events, stop: () => channel.unsubscribe(handlers) }; +} + +// Symbolic links can need extra privileges on Windows, so operations on +// them may fail there. +const mayFail = new Set(common.isWindows ? + ['lchmod', 'lchown', 'lutimes', 'readlink', 'symlink'] : []); + +function check(label, channelName, events, api) { + const starts = events.filter((e) => e.type === 'start' && e.context.api === api); + assert.strictEqual(starts.length, 1, + `${label} published ${starts.length} times with api '${api}'`); + const { context } = starts[0]; + const types = events.filter((e) => e.context === context).map((e) => e.type); + const expected = api === 'sync' ? + ['start', 'end'] : + ['start', 'end', 'asyncStart', 'asyncEnd']; + assert.deepStrictEqual(types.filter((t) => t !== 'error'), expected, label); + if (!mayFail.has(channelName)) { + assert.strictEqual(context.error, undefined, + `${label} failed: ${context.error?.message}`); + } +} + +async function run() { + for (const [name, channelName, call] of syncOps) { + if (!supported(name)) continue; + const fd = fs.openSync(newFile(), 'r+'); + const dir = fs.opendirSync(newDir()); + const { events, stop } = watch(channelName); + try { + call(fd, dir); + } catch { + // check() looks at the error. + } + stop(); + fs.closeSync(fd); + try { dir.closeSync(); } catch { /* Closed by the operation. */ } + check(`fs.${name}`, channelName, events, 'sync'); + } + + for (const [name, channelName, call] of callbackOps) { + if (!supported(name)) continue; + const fd = fs.openSync(newFile(), 'r+'); + const dir = fs.opendirSync(newDir()); + const { events, stop } = watch(channelName); + await new Promise((resolve) => call(() => setImmediate(resolve), fd, dir)); + stop(); + fs.closeSync(fd); + try { dir.closeSync(); } catch { /* Closed by the operation. */ } + check(`fs.${name}`, channelName, events, 'callback'); + } + + for (const [name, channelName, call] of promiseOps) { + if (!supported(name)) continue; + const fh = await fsp.open(newFile(), 'r+'); + const { events, stop } = watch(channelName); + try { + await call(fh); + } catch { + // check() looks at the error. + } + stop(); + await fh.close(); + check(`fs.promises.${name}`, channelName, events, 'promise'); + } +} + +run().then(common.mustCall()); diff --git a/test/parallel/test-diagnostics-channel-fs-promises.js b/test/parallel/test-diagnostics-channel-fs-promises.js new file mode 100644 index 000000000000..28e538cfbe33 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-promises.js @@ -0,0 +1,141 @@ +'use strict'; + +// Checks the events that fs/promises functions and FileHandle methods +// publish. + +const common = require('../common'); +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const file = tmpdir.resolve('file.txt'); +const missing = tmpdir.resolve('missing.txt'); +fs.writeFileSync(file, 'hello'); + +function record(name) { + const events = []; + const channel = dc.tracingChannel(`fs.${name}`); + const handlers = {}; + for (const type of ['start', 'end', 'asyncStart', 'asyncEnd', 'error']) { + handlers[type] = (context) => events.push({ type, context }); + } + channel.subscribe(handlers); + return { + events, + types: () => events.map((e) => e.type), + stop: () => channel.unsubscribe(handlers), + }; +} + +function assertSameContext(events) { + for (const { context } of events) { + assert.strictEqual(context, events[0].context); + } +} + +(async () => { + { + const stat = record('stat'); + const promise = fsp.stat(file); + const stats = await promise; + stat.stop(); + assert.deepStrictEqual(stat.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assertSameContext(stat.events); + const { context } = stat.events[0]; + assert.strictEqual(context.api, 'promise'); + assert.strictEqual(context.path, file); + assert.strictEqual(context.result, stats); + assert.ok(stats instanceof fs.Stats); + } + + { + const stat = record('stat'); + const err = await fsp.stat(missing).catch((err) => err); + stat.stop(); + assert.strictEqual(err.code, 'ENOENT'); + assert.deepStrictEqual(stat.types(), + ['start', 'end', 'error', 'asyncStart', 'asyncEnd']); + assertSameContext(stat.events); + assert.strictEqual(stat.events[0].context.error, err); + } + + { + // Invalid arguments are reported like other errors. + const stat = record('stat'); + await assert.rejects(fsp.stat(123), { code: 'ERR_INVALID_ARG_TYPE' }); + stat.stop(); + assert.deepStrictEqual(stat.types(), + ['start', 'end', 'error', 'asyncStart', 'asyncEnd']); + } + + { + const readFile = record('readFile'); + const data = await fsp.readFile(file, 'utf8'); + readFile.stop(); + assert.deepStrictEqual(readFile.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(readFile.events[0].context.path, file); + assert.strictEqual(readFile.events[0].context.result, data); + } + + { + const writeFile = record('writeFile'); + const data = Buffer.from('written'); + await fsp.writeFile(file, data); + writeFile.stop(); + assert.deepStrictEqual(writeFile.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(writeFile.events[0].context.path, file); + assert.strictEqual(writeFile.events[0].context.data, data); + } + + { + // FileHandle methods publish with the descriptor of the handle. + const open = record('open'); + const read = record('read'); + const fstat = record('fstat'); + const close = record('close'); + + const handle = await fsp.open(file, 'r'); + const { fd } = handle; + assert.deepStrictEqual(open.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(open.events[0].context.path, file); + assert.strictEqual(open.events[0].context.result, handle); + + const buffer = Buffer.alloc(7); + const result = await handle.read(buffer, 0, 7, 0); + assert.deepStrictEqual(read.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(read.events[0].context.api, 'promise'); + assert.strictEqual(read.events[0].context.fd, fd); + assert.deepStrictEqual(read.events[0].context.args, [buffer, 0, 7, 0]); + assert.strictEqual(read.events[0].context.result, result); + assert.strictEqual(result.bytesRead, 7); + + const stats = await handle.stat(); + assert.deepStrictEqual(fstat.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(fstat.events[0].context.fd, fd); + assert.strictEqual(fstat.events[0].context.result, stats); + + await handle.close(); + assert.deepStrictEqual(close.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(close.events[0].context.api, 'promise'); + assert.strictEqual(close.events[0].context.fd, fd); + + open.stop(); + read.stop(); + fstat.stop(); + close.stop(); + } + + { + const closedir = record('closedir'); + const dir = fs.opendirSync(tmpdir.path); + await dir.close(); + closedir.stop(); + assert.deepStrictEqual(closedir.types(), ['start', 'end', 'asyncStart', 'asyncEnd']); + assert.strictEqual(closedir.events[0].context.api, 'promise'); + assert.deepStrictEqual(closedir.events[0].context.args, []); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-diagnostics-channel-fs-sync.js b/test/parallel/test-diagnostics-channel-fs-sync.js new file mode 100644 index 000000000000..48293e15dc40 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-sync.js @@ -0,0 +1,171 @@ +'use strict'; + +// Checks the events that synchronous fs functions publish. + +const common = require('../common'); +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +// Captured before any subscriber exists. +const { statSync } = fs; + +function record(name) { + const events = []; + const channel = dc.tracingChannel(`fs.${name}`); + const handlers = {}; + for (const type of ['start', 'end', 'asyncStart', 'asyncEnd', 'error']) { + handlers[type] = (context) => events.push({ type, context }); + } + channel.subscribe(handlers); + return { + events, + types: () => events.map((e) => e.type), + stop: () => channel.unsubscribe(handlers), + }; +} + +function assertSameContext(events) { + for (const { context } of events) { + assert.strictEqual(context, events[0].context); + } +} + +const file = tmpdir.resolve('file.txt'); +const missing = tmpdir.resolve('missing.txt'); +fs.writeFileSync(file, 'hello'); + +{ + const stat = record('stat'); + const stats = statSync(file); + assert.deepStrictEqual(stat.types(), ['start', 'end']); + assertSameContext(stat.events); + const { context } = stat.events[0]; + assert.strictEqual(context.api, 'sync'); + assert.strictEqual(context.path, file); + assert.strictEqual(context.result, stats); + assert.ok(context.result instanceof fs.Stats); + stat.stop(); +} + +{ + // The error event comes before end. + const stat = record('stat'); + assert.throws(() => fs.statSync(missing), { code: 'ENOENT' }); + assert.deepStrictEqual(stat.types(), ['start', 'error', 'end']); + assertSameContext(stat.events); + assert.strictEqual(stat.events[0].context.error.code, 'ENOENT'); + assert.strictEqual(stat.events[0].context.path, missing); + stat.stop(); +} + +{ + // Invalid arguments are reported like other errors. + const stat = record('stat'); + assert.throws(() => fs.statSync(123), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.deepStrictEqual(stat.types(), ['start', 'error', 'end']); + assert.strictEqual(stat.events[0].context.error.code, 'ERR_INVALID_ARG_TYPE'); + stat.stop(); +} + +{ + const open = record('open'); + const read = record('read'); + const close = record('close'); + + const fd = fs.openSync(file, 'r'); + assert.deepStrictEqual(open.types(), ['start', 'end']); + assert.strictEqual(open.events[0].context.path, file); + assert.deepStrictEqual(open.events[0].context.args, [file, 'r']); + assert.strictEqual(open.events[0].context.result, fd); + + const buffer = Buffer.alloc(5); + const bytesRead = fs.readSync(fd, buffer, 0, 5, 0); + assert.deepStrictEqual(read.types(), ['start', 'end']); + const readContext = read.events[0].context; + assert.strictEqual(readContext.fd, fd); + assert.strictEqual(readContext.args[1], buffer); + assert.deepStrictEqual(readContext.args.slice(2), [0, 5, 0]); + assert.strictEqual(readContext.result, bytesRead); + + fs.closeSync(fd); + assert.deepStrictEqual(close.types(), ['start', 'end']); + assert.strictEqual(close.events[0].context.fd, fd); + + open.stop(); + read.stop(); + close.stop(); +} + +{ + const writeFile = record('writeFile'); + const readFile = record('readFile'); + const data = 'new contents'; + + fs.writeFileSync(file, data); + assert.deepStrictEqual(writeFile.types(), ['start', 'end']); + assert.strictEqual(writeFile.events[0].context.path, file); + assert.strictEqual(writeFile.events[0].context.data, data); + + // The utf8 fast path and the generic path both publish. + const text = fs.readFileSync(file, 'utf8'); + const buffer = fs.readFileSync(file); + assert.deepStrictEqual(readFile.types(), ['start', 'end', 'start', 'end']); + assert.strictEqual(readFile.events[1].context.result, text); + assert.strictEqual(readFile.events[3].context.result, buffer); + + writeFile.stop(); + readFile.stop(); +} + +{ + const exists = record('exists'); + assert.strictEqual(fs.existsSync(file), true); + assert.strictEqual(fs.existsSync(missing), false); + assert.deepStrictEqual(exists.types(), ['start', 'end', 'start', 'end']); + assert.strictEqual(exists.events[1].context.result, true); + assert.strictEqual(exists.events[3].context.result, false); + exists.stop(); +} + +{ + // A subscriber that changes `context.args` does not change the call. + const channel = dc.tracingChannel('fs.stat'); + const onStart = common.mustCall((context) => { + context.args[0] = tmpdir.resolve('does-not-exist'); + }); + channel.start.subscribe(onStart); + assert.ok(fs.statSync(file).isFile()); + channel.start.unsubscribe(onStart); +} + +{ + // A replaced `realpathSync.native` runs once per traced call, because + // the trace calls the original function, not the property. + const original = fs.realpathSync.native; + const expected = fs.realpathSync(file); + let calls = 0; + fs.realpathSync.native = function(path, options) { + calls++; + return original(path, options); + }; + const realpath = record('realpath'); + assert.strictEqual(fs.realpathSync.native(file), expected); + realpath.stop(); + fs.realpathSync.native = original; + assert.strictEqual(calls, 1); + assert.deepStrictEqual(realpath.types(), ['start', 'end']); +} + +{ + const rename = record('rename'); + const dest = tmpdir.resolve('renamed.txt'); + fs.renameSync(file, dest); + assert.deepStrictEqual(rename.types(), ['start', 'end']); + assert.strictEqual(rename.events[0].context.path, file); + assert.strictEqual(rename.events[0].context.dest, dest); + rename.stop(); +}