diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index d250817aa203..5c8d2c8a900b 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1636,6 +1636,90 @@ diagnosticsChannel.subscribe('crypto.fips.indicator', (message) => { }); ``` +#### File system + + + +> Stability: 1 - Experimental + +Each `node:fs` operation publishes to its own [`TracingChannel`][] named +`fs.`, for example `tracing:fs.stat:start`. The synchronous, +callback, and promise forms of an operation share the same channel. + +* `event` {Object} containing the following properties + * `api` {string} The form of the function: `'sync'`, `'callback'`, or + `'promise'`. + * `args` {Array} The arguments as passed by the caller. + * The leading arguments of the operation, by name, as listed in the table + below. For `FileHandle` methods, `fd` is the descriptor of the handle and + `args` holds the arguments of the method. + +| Channel | Functions | Named 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.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` | + +A function name such as `stat` means `fs.stat()`, `fs.statSync()`, and +`fsPromises.stat()`, where these exist. For `fs.readFile`, `fs.writeFile`, and +`fs.appendFile`, `path` is the value as passed, so it can also be a file +descriptor or a {FileHandle}. For `fs.write` with a string, `buffer` is the +string. For callback forms, `result` is the first callback argument after the +error, so for `fs.read` and `fs.write` it is the number of bytes. + +An operation that uses other `fs` operations also publishes those, inside its +own events. For example, `fs.readFileSync()` without an encoding publishes to +`fs.open`, `fs.read`, and `fs.close`. Streams from `fs.createReadStream()` and +`fs.createWriteStream()` publish the `fs.open`, `fs.read`, `fs.write`, +`fs.writev`, and `fs.close` operations they use. + +`fsPromises.glob()`, `fs.watch()`, `fs.watchFile()`, and `dir.read()` do not +publish events. + #### HTTP > Stability: 1 - Experimental diff --git a/lib/fs.js b/lib/fs.js index 4436fa2df6e6..d26881533367 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -115,9 +115,13 @@ const { getStatFsFromBinding, getStatsFromBinding, realpathCacheKey, + shouldTraceFs, stringToFlags, stringToSymlinkType, toUnixTimestamp, + traceFsCallback, + traceFsPromise, + traceFsSync, validateBufferArray, validateCpOptions, validateOffsetLengthRead, @@ -241,6 +245,7 @@ function isFileType(stats, fileType) { * @returns {void} */ function access(path, mode, callback) { + if (shouldTraceFs('access')) return traceFsCallback('access', access, this, arguments); if (typeof mode === 'function') { callback = mode; mode = F_OK; @@ -265,6 +270,7 @@ function access(path, mode, callback) { * @returns {void} */ function accessSync(path, mode) { + if (shouldTraceFs('access')) return traceFsSync('access', accessSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.accessSync(path, mode); @@ -281,6 +287,11 @@ function accessSync(path, mode) { */ function exists(path, callback) { validateFunction(callback, 'cb'); + if (shouldTraceFs('exists')) { + // The callback of fs.exists() takes only the result. + return traceFsCallback('exists', existsErrorFirst, this, + [path, (err, result) => callback(result)], arguments); + } const h = vfsState.handlers; if (h !== null) { @@ -302,6 +313,10 @@ function exists(path, callback) { } } +function existsErrorFirst(path, callback) { + exists(path, (result) => callback(null, result)); +} + ObjectDefineProperty(exists, kCustomPromisifiedSymbol, { __proto__: null, value: function exists(path) { @@ -316,6 +331,7 @@ let showExistsDeprecation = true; * @returns {boolean} */ function existsSync(path) { + if (shouldTraceFs('exists')) return traceFsSync('exists', existsSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.existsSync(path); @@ -419,6 +435,7 @@ function bindSignalToReq(req, signal, callback) { * @returns {void} */ function readFile(path, options, callback) { + if (shouldTraceFs('readFile')) return traceFsCallback('readFile', readFile, this, arguments); callback ||= options; validateFunction(callback, 'cb'); @@ -602,6 +619,7 @@ function tryReadSyncWithUserBuffer(fd, isUserFd, buffer, byteLengthName) { * @returns {string | Buffer} */ function readFileSync(path, options) { + if (shouldTraceFs('readFile')) return traceFsSync('readFile', readFileSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.readFileSync(path, options); @@ -701,6 +719,10 @@ function defaultCloseCallback(err) { * @returns {void} */ function close(fd, callback = defaultCloseCallback) { + if (shouldTraceFs('close')) { + // Passes the default callback, so that the call has one to trace. + return traceFsCallback('close', close, this, [fd, callback], arguments); + } if (callback !== defaultCloseCallback) callback = makeCallback(callback); @@ -718,6 +740,7 @@ function close(fd, callback = defaultCloseCallback) { * @returns {void} */ function closeSync(fd) { + if (shouldTraceFs('close')) return traceFsSync('close', closeSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.closeSync(fd); @@ -738,6 +761,7 @@ function closeSync(fd) { * @returns {void} */ function open(path, flags, mode, callback) { + if (shouldTraceFs('open')) return traceFsCallback('open', open, this, arguments); if (arguments.length < 3) { callback = flags; flags = 'r'; @@ -771,6 +795,7 @@ function open(path, flags, mode, callback) { * @returns {number} */ function openSync(path, flags, mode) { + if (shouldTraceFs('open')) return traceFsSync('open', openSync, this, arguments); flags = stringToFlags(flags); mode = parseFileMode(mode, 'mode', 0o666); const h = vfsState.handlers; @@ -793,6 +818,7 @@ function openSync(path, flags, mode) { * @returns {Promise} */ function openAsBlob(path, options = kEmptyObject) { + if (shouldTraceFs('openAsBlob')) return traceFsPromise('openAsBlob', openAsBlob, this, arguments); validateObject(options, 'options'); const { type = '' } = options; validateString(type, 'options.type'); @@ -819,6 +845,7 @@ function openAsBlob(path, options = kEmptyObject) { * @returns {Blob} */ function openAsBlobSync(path, options = kEmptyObject) { + if (shouldTraceFs('openAsBlob')) return traceFsSync('openAsBlob', openAsBlobSync, this, arguments); validateObject(options, 'options'); const { type = '' } = options; validateString(type, 'options.type'); @@ -853,6 +880,7 @@ function openAsBlobSync(path, options = kEmptyObject) { * @returns {void} */ function read(fd, buffer, offsetOrOptions, length, position, callback) { + if (shouldTraceFs('read')) return traceFsCallback('read', read, this, arguments); fd = getValidatedFd(fd); let offset = offsetOrOptions; let params = null; @@ -955,6 +983,7 @@ ObjectDefineProperty(read, kCustomPromisifyArgsSymbol, * @returns {number} */ function readSync(fd, buffer, offsetOrOptions, length, position) { + if (shouldTraceFs('read')) return traceFsSync('read', readSync, this, arguments); validateBuffer(buffer); let offset = offsetOrOptions; @@ -1018,6 +1047,7 @@ function readSync(fd, buffer, offsetOrOptions, length, position) { * @returns {void} */ function readv(fd, buffers, position, callback) { + if (shouldTraceFs('readv')) return traceFsCallback('readv', readv, this, arguments); function wrapper(err, read) { callback(err, read || 0, buffers); } @@ -1059,6 +1089,7 @@ ObjectDefineProperty(readv, kCustomPromisifyArgsSymbol, * @returns {number} */ function readvSync(fd, buffers, position) { + if (shouldTraceFs('readv')) return traceFsSync('readv', readvSync, this, arguments); validateBufferArray(buffers); if (typeof position !== 'number') @@ -1088,6 +1119,7 @@ function readvSync(fd, buffers, position) { * @returns {void} */ function write(fd, buffer, offsetOrOptions, length, position, callback) { + if (shouldTraceFs('write')) return traceFsCallback('write', write, this, 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 +1217,7 @@ ObjectDefineProperty(write, kCustomPromisifyArgsSymbol, * @returns {number} */ function writeSync(fd, buffer, offsetOrOptions, length, position) { + if (shouldTraceFs('write')) return traceFsSync('write', writeSync, this, arguments); const ctx = {}; let result; @@ -1251,6 +1284,7 @@ function writeSync(fd, buffer, offsetOrOptions, length, position) { * @returns {void} */ function writev(fd, buffers, position, callback) { + if (shouldTraceFs('writev')) return traceFsCallback('writev', writev, this, arguments); function wrapper(err, written) { callback(err, written || 0, buffers); } @@ -1299,6 +1333,7 @@ ObjectDefineProperty(writev, kCustomPromisifyArgsSymbol, { * @returns {number} */ function writevSync(fd, buffers, position) { + if (shouldTraceFs('writev')) return traceFsSync('writev', writevSync, this, arguments); validateBufferArray(buffers); if (buffers.length === 0) { @@ -1326,6 +1361,7 @@ function writevSync(fd, buffers, position) { * @returns {void} */ function rename(oldPath, newPath, callback) { + if (shouldTraceFs('rename')) return traceFsCallback('rename', rename, this, arguments); const h = vfsState.handlers; if (h !== null && vfsVoid(h.rename(oldPath, newPath), callback)) return; @@ -1348,6 +1384,7 @@ function rename(oldPath, newPath, callback) { * @returns {void} */ function renameSync(oldPath, newPath) { + if (shouldTraceFs('rename')) return traceFsSync('rename', renameSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.renameSync(oldPath, newPath); @@ -1367,6 +1404,7 @@ function renameSync(oldPath, newPath) { * @returns {void} */ function truncate(path, len, callback) { + if (shouldTraceFs('truncate')) return traceFsCallback('truncate', truncate, this, arguments); if (typeof len === 'function') { callback = len; len = 0; @@ -1400,6 +1438,7 @@ function truncate(path, len, callback) { * @returns {void} */ function truncateSync(path, len) { + if (shouldTraceFs('truncate')) return traceFsSync('truncate', truncateSync, this, arguments); if (len === undefined) { len = 0; } @@ -1427,6 +1466,7 @@ function truncateSync(path, len) { * @returns {void} */ function ftruncate(fd, len = 0, callback) { + if (shouldTraceFs('ftruncate')) return traceFsCallback('ftruncate', ftruncate, this, arguments); if (typeof len === 'function') { callback = len; len = 0; @@ -1450,6 +1490,7 @@ function ftruncate(fd, len = 0, callback) { * @returns {void} */ function ftruncateSync(fd, len = 0) { + if (shouldTraceFs('ftruncate')) return traceFsSync('ftruncate', ftruncateSync, this, arguments); validateInteger(len, 'len'); const h = vfsState.handlers; @@ -1482,6 +1523,7 @@ function lazyLoadRimraf() { * @returns {void} */ function rmdir(path, options, callback) { + if (shouldTraceFs('rmdir')) return traceFsCallback('rmdir', rmdir, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -1518,6 +1560,7 @@ function rmdir(path, options, callback) { * @returns {void} */ function rmdirSync(path, options) { + if (shouldTraceFs('rmdir')) return traceFsSync('rmdir', rmdirSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.rmdirSync(path); @@ -1551,6 +1594,7 @@ function rmdirSync(path, options) { * @returns {void} */ function rm(path, options, callback) { + if (shouldTraceFs('rm')) return traceFsCallback('rm', rm, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -1583,6 +1627,7 @@ function rm(path, options, callback) { * @returns {void} */ function rmSync(path, options) { + if (shouldTraceFs('rm')) return traceFsSync('rm', rmSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.rmSync(path, options); @@ -1601,6 +1646,7 @@ function rmSync(path, options) { * @returns {void} */ function fdatasync(fd, callback) { + if (shouldTraceFs('fdatasync')) return traceFsCallback('fdatasync', fdatasync, this, arguments); callback = makeCallback(callback); const h = vfsState.handlers; @@ -1624,6 +1670,7 @@ function fdatasync(fd, callback) { * @returns {void} */ function fdatasyncSync(fd) { + if (shouldTraceFs('fdatasync')) return traceFsSync('fdatasync', fdatasyncSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.fdatasyncSync(fd); @@ -1644,6 +1691,7 @@ function fdatasyncSync(fd) { * @returns {void} */ function fsync(fd, callback) { + if (shouldTraceFs('fsync')) return traceFsCallback('fsync', fsync, this, arguments); callback = makeCallback(callback); const h = vfsState.handlers; @@ -1665,6 +1713,7 @@ function fsync(fd, callback) { * @returns {void} */ function fsyncSync(fd) { + if (shouldTraceFs('fsync')) return traceFsSync('fsync', fsyncSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.fsyncSync(fd); @@ -1688,6 +1737,7 @@ function fsyncSync(fd) { * @returns {void} */ function mkdir(path, options, callback) { + if (shouldTraceFs('mkdir')) return traceFsCallback('mkdir', mkdir, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -1737,6 +1787,7 @@ function mkdir(path, options, callback) { * @returns {string | void} */ function mkdirSync(path, options) { + if (shouldTraceFs('mkdir')) return traceFsSync('mkdir', mkdirSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const vfsResult = h.mkdirSync(path, options); @@ -1885,6 +1936,7 @@ function readdirSyncRecursive(basePath, options) { * @returns {void} */ function readdir(path, options, callback) { + if (shouldTraceFs('readdir')) return traceFsCallback('readdir', readdir, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -1936,6 +1988,7 @@ function readdir(path, options, callback) { * @returns {string | Buffer[] | Dirent[]} */ function readdirSync(path, options) { + if (shouldTraceFs('readdir')) return traceFsSync('readdir', readdirSync, this, arguments); options = getOptions(options); path = getValidatedPath(path); if (options.recursive != null) { @@ -1973,6 +2026,7 @@ function readdirSync(path, options) { * @returns {void} */ function fstat(fd, options = { __proto__: null, bigint: false }, callback) { + if (shouldTraceFs('fstat')) return traceFsCallback('fstat', fstat, this, arguments); if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2005,6 +2059,7 @@ function fstat(fd, options = { __proto__: null, bigint: false }, callback) { * @returns {void} */ function lstat(path, options = { __proto__: null, bigint: false }, callback) { + if (shouldTraceFs('lstat')) return traceFsCallback('lstat', lstat, this, arguments); if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2045,6 +2100,7 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) { * @returns {void} */ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }, callback) { + if (shouldTraceFs('stat')) return traceFsCallback('stat', stat, this, arguments); if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2069,6 +2125,7 @@ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: } function statfs(path, options = { __proto__: null, bigint: false }, callback) { + if (shouldTraceFs('statfs')) return traceFsCallback('statfs', statfs, this, arguments); if (typeof options === 'function') { callback = options; options = kEmptyObject; @@ -2110,6 +2167,7 @@ function statfs(path, options = { __proto__: null, bigint: false }, callback) { * @returns {Stats | undefined} */ function fstatSync(fd, options = { __proto__: null, bigint: false }) { + if (shouldTraceFs('fstat')) return traceFsSync('fstat', fstatSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.fstatSync(fd); @@ -2133,6 +2191,7 @@ function fstatSync(fd, options = { __proto__: null, bigint: false }) { * @returns {Stats | undefined} */ function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (shouldTraceFs('lstat')) return traceFsSync('lstat', lstatSync, this, arguments); const h = vfsState.handlers; if (h !== null) { try { @@ -2173,6 +2232,7 @@ function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEn * @returns {Stats} */ function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (shouldTraceFs('stat')) return traceFsSync('stat', statSync, this, arguments); const h = vfsState.handlers; if (h !== null) { try { @@ -2196,6 +2256,7 @@ function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEnt } function statfsSync(path, options = { __proto__: null, bigint: false }) { + if (shouldTraceFs('statfs')) return traceFsSync('statfs', statfsSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.statfsSync(path, options); @@ -2218,6 +2279,7 @@ function statfsSync(path, options = { __proto__: null, bigint: false }) { * @returns {void} */ function readlink(path, options, callback) { + if (shouldTraceFs('readlink')) return traceFsCallback('readlink', readlink, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -2241,6 +2303,7 @@ function readlink(path, options, callback) { * @returns {string | Buffer} */ function readlinkSync(path, options) { + if (shouldTraceFs('readlink')) return traceFsSync('readlink', readlinkSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.readlinkSync(path, options); @@ -2259,6 +2322,7 @@ function readlinkSync(path, options) { * @returns {void} */ function symlink(target, path, type, callback) { + if (shouldTraceFs('symlink')) return traceFsCallback('symlink', symlink, this, arguments); if (callback === undefined) { callback = makeCallback(type); type = undefined; @@ -2331,6 +2395,7 @@ function symlink(target, path, type, callback) { * @returns {void} */ function symlinkSync(target, path, type) { + if (shouldTraceFs('symlink')) return traceFsSync('symlink', symlinkSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.symlinkSync(target, path, type); @@ -2371,6 +2436,7 @@ function symlinkSync(target, path, type) { * @returns {void} */ function link(existingPath, newPath, callback) { + if (shouldTraceFs('link')) return traceFsCallback('link', link, this, arguments); callback = makeCallback(callback); const h = vfsState.handlers; @@ -2393,6 +2459,7 @@ function link(existingPath, newPath, callback) { * @returns {void} */ function linkSync(existingPath, newPath) { + if (shouldTraceFs('link')) return traceFsSync('link', linkSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.linkSync(existingPath, newPath); @@ -2415,6 +2482,7 @@ function linkSync(existingPath, newPath) { * @returns {void} */ function unlink(path, callback) { + if (shouldTraceFs('unlink')) return traceFsCallback('unlink', unlink, this, arguments); const h = vfsState.handlers; if (h !== null && vfsVoid(h.unlink(path), callback)) return; @@ -2430,6 +2498,7 @@ function unlink(path, callback) { * @returns {void} */ function unlinkSync(path) { + if (shouldTraceFs('unlink')) return traceFsSync('unlink', unlinkSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.unlinkSync(path); @@ -2446,6 +2515,7 @@ function unlinkSync(path) { * @returns {void} */ function fchmod(fd, mode, callback) { + if (shouldTraceFs('fchmod')) return traceFsCallback('fchmod', fchmod, this, arguments); mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); @@ -2469,6 +2539,7 @@ function fchmod(fd, mode, callback) { * @returns {void} */ function fchmodSync(fd, mode) { + if (shouldTraceFs('fchmod')) return traceFsSync('fchmod', fchmodSync, this, arguments); mode = parseFileMode(mode, 'mode'); const h = vfsState.handlers; @@ -2491,6 +2562,7 @@ function fchmodSync(fd, mode) { * @returns {void} */ function lchmod(path, mode, callback) { + if (shouldTraceFs('lchmod')) return traceFsCallback('lchmod', lchmod, this, arguments); validateFunction(callback, 'cb'); mode = parseFileMode(mode, 'mode'); @@ -2519,6 +2591,7 @@ function lchmod(path, mode, callback) { * @returns {void} */ function lchmodSync(path, mode) { + if (shouldTraceFs('lchmod')) return traceFsSync('lchmod', lchmodSync, this, arguments); const fd = fs.openSync(path, O_WRONLY | O_SYMLINK); // Prefer to return the chmod error, if one occurs, @@ -2538,6 +2611,7 @@ function lchmodSync(path, mode) { * @returns {void} */ function chmod(path, mode, callback) { + if (shouldTraceFs('chmod')) return traceFsCallback('chmod', chmod, this, arguments); path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); @@ -2557,6 +2631,7 @@ function chmod(path, mode, callback) { * @returns {void} */ function chmodSync(path, mode) { + if (shouldTraceFs('chmod')) return traceFsSync('chmod', chmodSync, this, arguments); path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); @@ -2578,6 +2653,7 @@ function chmodSync(path, mode) { * @returns {void} */ function lchown(path, uid, gid, callback) { + if (shouldTraceFs('lchown')) return traceFsCallback('lchown', lchown, this, arguments); callback = makeCallback(callback); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); @@ -2599,6 +2675,7 @@ function lchown(path, uid, gid, callback) { * @returns {void} */ function lchownSync(path, uid, gid) { + if (shouldTraceFs('lchown')) return traceFsSync('lchown', lchownSync, this, arguments); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2621,6 +2698,7 @@ function lchownSync(path, uid, gid) { * @returns {void} */ function fchown(fd, uid, gid, callback) { + if (shouldTraceFs('fchown')) return traceFsCallback('fchown', fchown, this, arguments); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); callback = makeCallback(callback); @@ -2646,6 +2724,7 @@ function fchown(fd, uid, gid, callback) { * @returns {void} */ function fchownSync(fd, uid, gid) { + if (shouldTraceFs('fchown')) return traceFsSync('fchown', fchownSync, this, arguments); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2672,6 +2751,7 @@ function fchownSync(fd, uid, gid) { * @returns {void} */ function chown(path, uid, gid, callback) { + if (shouldTraceFs('chown')) return traceFsCallback('chown', chown, this, arguments); callback = makeCallback(callback); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); @@ -2694,6 +2774,7 @@ function chown(path, uid, gid, callback) { * @returns {void} */ function chownSync(path, uid, gid) { + if (shouldTraceFs('chown')) return traceFsSync('chown', chownSync, this, arguments); path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -2717,6 +2798,7 @@ function chownSync(path, uid, gid) { * @returns {void} */ function utimes(path, atime, mtime, callback) { + if (shouldTraceFs('utimes')) return traceFsCallback('utimes', utimes, this, arguments); callback = makeCallback(callback); path = getValidatedPath(path); atime = toUnixTimestamp(atime); @@ -2739,6 +2821,7 @@ function utimes(path, atime, mtime, callback) { * @returns {void} */ function utimesSync(path, atime, mtime) { + if (shouldTraceFs('utimes')) return traceFsSync('utimes', utimesSync, this, arguments); path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2762,6 +2845,7 @@ function utimesSync(path, atime, mtime) { * @returns {void} */ function futimes(fd, atime, mtime, callback) { + if (shouldTraceFs('futimes')) return traceFsCallback('futimes', futimes, this, arguments); atime = toUnixTimestamp(atime, 'atime'); mtime = toUnixTimestamp(mtime, 'mtime'); callback = makeCallback(callback); @@ -2789,6 +2873,7 @@ function futimes(fd, atime, mtime, callback) { * @returns {void} */ function futimesSync(fd, atime, mtime) { + if (shouldTraceFs('futimes')) return traceFsSync('futimes', futimesSync, this, arguments); atime = toUnixTimestamp(atime, 'atime'); mtime = toUnixTimestamp(mtime, 'mtime'); @@ -2815,6 +2900,7 @@ function futimesSync(fd, atime, mtime) { * @returns {void} */ function lutimes(path, atime, mtime, callback) { + if (shouldTraceFs('lutimes')) return traceFsCallback('lutimes', lutimes, this, arguments); callback = makeCallback(callback); path = getValidatedPath(path); atime = toUnixTimestamp(atime); @@ -2837,6 +2923,7 @@ function lutimes(path, atime, mtime, callback) { * @returns {void} */ function lutimesSync(path, atime, mtime) { + if (shouldTraceFs('lutimes')) return traceFsSync('lutimes', lutimesSync, this, arguments); path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2919,6 +3006,7 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) * @returns {void} */ function writeFile(path, data, options, callback) { + if (shouldTraceFs('writeFile')) return traceFsCallback('writeFile', writeFile, this, arguments); callback ||= options; validateFunction(callback, 'cb'); @@ -2996,6 +3084,7 @@ function writeFile(path, data, options, callback) { * @returns {void} */ function writeFileSync(path, data, options) { + if (shouldTraceFs('writeFile')) return traceFsSync('writeFile', writeFileSync, this, arguments); options = getOptions(options, { encoding: 'utf8', mode: 0o666, @@ -3071,6 +3160,7 @@ function writeFileSync(path, data, options) { * @returns {void} */ function appendFile(path, data, options, callback) { + if (shouldTraceFs('appendFile')) return traceFsCallback('appendFile', appendFile, this, arguments); callback ||= options; validateFunction(callback, 'cb'); @@ -3107,6 +3197,7 @@ function appendFile(path, data, options, callback) { * @returns {void} */ function appendFileSync(path, data, options) { + if (shouldTraceFs('appendFile')) return traceFsSync('appendFile', appendFileSync, this, arguments); options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); parseFileMode(options.mode, 'mode', 0o666); @@ -3355,6 +3446,7 @@ if (isWindows) { * @returns {string | Buffer} */ function realpathSync(p, options) { + if (shouldTraceFs('realpath')) return traceFsSync('realpath', realpathSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.realpathSync(p, options); @@ -3503,7 +3595,10 @@ function realpathSync(p, options) { * @param {string | { encoding?: string; }} [options] * @returns {string | Buffer} */ -realpathSync.native = (path, options) => { +realpathSync.native = function(path, options) { + if (shouldTraceFs('realpath')) { + return traceFsSync('realpath', realpathSync.native, undefined, arguments); + } const h = vfsState.handlers; if (h !== null) { const result = h.realpathSync(path, options); @@ -3528,6 +3623,7 @@ realpathSync.native = (path, options) => { * @returns {void} */ function realpath(p, options, callback) { + if (shouldTraceFs('realpath')) return traceFsCallback('realpath', realpath, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -3680,7 +3776,10 @@ function realpath(p, options, callback) { * ) => any} callback * @returns {void} */ -realpath.native = (path, options, callback) => { +realpath.native = function(path, options, callback) { + if (shouldTraceFs('realpath')) { + return traceFsCallback('realpath', realpath.native, undefined, arguments); + } callback = makeCallback(callback || options); const h = vfsState.handlers; if (h !== null && vfsResult(h.realpath(path, options), callback)) return; @@ -3702,6 +3801,7 @@ realpath.native = (path, options, callback) => { * @returns {void} */ function mkdtemp(prefix, options, callback) { + if (shouldTraceFs('mkdtemp')) return traceFsCallback('mkdtemp', mkdtemp, this, arguments); callback = makeCallback(typeof options === 'function' ? options : callback); const h = vfsState.handlers; @@ -3726,6 +3826,7 @@ function mkdtemp(prefix, options, callback) { * @returns {string} */ function mkdtempSync(prefix, options) { + if (shouldTraceFs('mkdtemp')) return traceFsSync('mkdtemp', mkdtempSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.mkdtempSync(prefix, options); @@ -3750,6 +3851,7 @@ function mkdtempSync(prefix, options) { * @returns {object} A disposable object with a "path" property. */ function mkdtempDisposableSync(prefix, options) { + if (shouldTraceFs('mkdtemp')) return traceFsSync('mkdtemp', mkdtempDisposableSync, this, arguments); options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -3783,6 +3885,7 @@ function mkdtempDisposableSync(prefix, options) { * @returns {void} */ function copyFile(src, dest, mode, callback) { + if (shouldTraceFs('copyFile')) return traceFsCallback('copyFile', copyFile, this, arguments); if (typeof mode === 'function') { callback = mode; mode = 0; @@ -3809,6 +3912,7 @@ function copyFile(src, dest, mode, callback) { * @returns {void} */ function copyFileSync(src, dest, mode) { + if (shouldTraceFs('copyFile')) return traceFsSync('copyFile', copyFileSync, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.copyFileSync(src, dest, mode); @@ -3831,6 +3935,7 @@ function copyFileSync(src, dest, mode) { * @returns {void} */ function cp(src, dest, options, callback) { + if (shouldTraceFs('cp')) return traceFsCallback('cp', cp, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -3852,6 +3957,7 @@ function cp(src, dest, options, callback) { * @returns {void} */ function cpSync(src, dest, options) { + if (shouldTraceFs('cp')) return traceFsSync('cp', cpSync, this, arguments); options = validateCpOptions(options); src = getValidatedPath(src, 'src'); dest = getValidatedPath(dest, 'dest'); @@ -3927,6 +4033,7 @@ function createWriteStream(path, options) { const lazyGlob = getLazy(() => require('internal/fs/glob').Glob); function glob(pattern, options, callback) { + if (shouldTraceFs('glob')) return traceFsCallback('glob', glob, this, arguments); if (typeof options === 'function') { callback = options; options = undefined; @@ -3942,6 +4049,7 @@ function glob(pattern, options, callback) { } function globSync(pattern, options) { + if (shouldTraceFs('glob')) return traceFsSync('glob', globSync, this, 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..27677a3a521d 100644 --- a/lib/internal/fs/dir.js +++ b/lib/internal/fs/dir.js @@ -29,7 +29,11 @@ const { } = require('internal/util'); const { getDirent, + getFsChannel, getOptions, + shouldTraceFs, + traceFsCallback, + traceFsSync, getValidatedPath, vfsState, } = require('internal/fs/utils'); @@ -71,7 +75,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 +250,28 @@ class Dir { if (this.#closed === true) { return PromiseReject(new ERR_DIR_CLOSED()); } - return this.#closePromisified(); + return this.#closePromise(); } validateFunction(callback, 'callback'); + if (getFsChannel('closedir').hasSubscribers) { + const context = { api: 'callback', args: [callback], path: this.#path }; + return getFsChannel('closedir').traceCallback(this.#closeImpl, -1, context, this, + callback); + } + this.#closeImpl(callback); + } + + #closePromise() { + if (getFsChannel('closedir').hasSubscribers) { + const context = { api: 'promise', args: [], path: this.#path }; + return getFsChannel('closedir').tracePromise(this.#closePromisified, context); + } + return this.#closePromisified(); + } + + #closeImpl(callback) { if (this.#closed === true) { process.nextTick(callback, new ERR_DIR_CLOSED()); return; @@ -258,7 +279,7 @@ class Dir { if (this.#operationQueue !== null) { ArrayPrototypePush(this.#operationQueue, () => { - this.close(callback); + this.#closeImpl(callback); }); return; } @@ -289,6 +310,11 @@ class Dir { } this.#closed = true; + if (getFsChannel('closedir').hasSubscribers) { + const context = { api: 'sync', args: [], path: this.#path }; + getFsChannel('closedir').traceSync(this.#handle.close, context, this.#handle); + return; + } this.#handle.close(); } @@ -302,7 +328,7 @@ class Dir { yield result; } } finally { - await this.#closePromisified(); + await this.#closePromise(); } } @@ -313,7 +339,7 @@ class Dir { async [SymbolAsyncDispose]() { if (this.#closed) return; - await this.#closePromisified(); + await this.#closePromise(); } } @@ -328,6 +354,7 @@ ObjectDefineProperties(Dir.prototype, { }); function opendir(path, options, callback) { + if (shouldTraceFs('opendir')) return traceFsCallback('opendir', opendir, this, arguments); callback = typeof options === 'function' ? options : callback; validateFunction(callback, 'callback'); @@ -369,6 +396,7 @@ function opendir(path, options, callback) { } function opendirSync(path, options) { + if (shouldTraceFs('opendir')) return traceFsSync('opendir', opendirSync, this, 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 efa981c55e31..c80ae7d44941 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -65,6 +65,7 @@ const { collectRecursiveReaddirResult, copyObject, getDirents, + getFsChannel, getRecursiveDirents, getOptions, getStatFsFromBinding, @@ -73,10 +74,13 @@ const { getReadFileBuffer, getReadFileBufferByteLengthName, preprocessSymlinkDestination, + shouldTraceFs, stringToFlags, stringToSymlinkType, toUnixTimestamp, handleErrorFromBinding: handleSyncErrorFromBinding, + callUntraced, + traceFsPromise, validateBufferArray, validateCpOptions, validateOffsetLengthRead, @@ -89,7 +93,7 @@ const { vfsState, warnOnNonPortableTemplate, } = require('internal/fs/utils'); -const { opendir } = require('internal/fs/dir'); +const { opendir: opendirCallback } = require('internal/fs/dir'); const { parseFileMode, validateAbortSignal, @@ -187,6 +191,14 @@ function handleErrorFromBinding(error) { return PromiseReject(error); } +function closeFileHandle(handle, fd) { + if (getFsChannel('close').hasSubscribers) { + const context = { api: 'promise', args: [], fd }; + return getFsChannel('close').tracePromise(handle.close, context, handle); + } + return handle.close(); +} + class FileHandle extends EventEmitter { #brandCheck = undefined; @@ -289,9 +301,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 +329,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 (getFsChannel('close').hasSubscribers) { + const context = { api: 'sync', args: [], fd }; + getFsChannel('close').traceSync(this[kHandle].closeSync, context, this[kHandle]); + } else { + this[kHandle].closeSync(); + } this.emit('close'); } @@ -466,9 +485,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], ); @@ -1374,6 +1394,7 @@ 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 (shouldTraceFs('access')) return await traceFsPromise('access', access, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.access(path, mode); @@ -1387,6 +1408,7 @@ async function access(path, mode = F_OK) { } async function cp(src, dest, options) { + if (shouldTraceFs('cp')) return await traceFsPromise('cp', cp, this, arguments); options = validateCpOptions(options); src = getValidatedPath(src, 'src'); dest = getValidatedPath(dest, 'dest'); @@ -1394,6 +1416,7 @@ async function cp(src, dest, options) { } async function copyFile(src, dest, mode) { + if (shouldTraceFs('copyFile')) return await traceFsPromise('copyFile', copyFile, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.copyFile(src, dest, mode); @@ -1414,6 +1437,7 @@ 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 (shouldTraceFs('open')) return await traceFsPromise('open', open, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.promisesOpen(path, flags, mode); @@ -1430,6 +1454,7 @@ async function open(path, flags, mode) { } async function read(handle, bufferOrParams, offset, length, position) { + if (shouldTraceFs('read')) return await traceFsPromise('read', read, this, arguments); let buffer = bufferOrParams; if (!isArrayBufferView(buffer)) { // This is fh.read(params) @@ -1490,6 +1515,7 @@ async function read(handle, bufferOrParams, offset, length, position) { } async function readv(handle, buffers, position) { + if (shouldTraceFs('readv')) return await traceFsPromise('readv', readv, this, arguments); validateBufferArray(buffers); if (typeof position !== 'number') @@ -1504,6 +1530,7 @@ async function readv(handle, buffers, position) { } async function write(handle, buffer, offsetOrOptions, length, position) { + if (shouldTraceFs('write')) return await traceFsPromise('write', write, this, arguments); if (buffer?.byteLength === 0) return { __proto__: null, bytesWritten: 0, buffer }; @@ -1548,6 +1575,7 @@ async function write(handle, buffer, offsetOrOptions, length, position) { } async function writev(handle, buffers, position) { + if (shouldTraceFs('writev')) return await traceFsPromise('writev', writev, this, arguments); validateBufferArray(buffers); if (typeof position !== 'number') @@ -1566,6 +1594,7 @@ async function writev(handle, buffers, position) { } async function rename(oldPath, newPath) { + if (shouldTraceFs('rename')) return await traceFsPromise('rename', rename, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.rename(oldPath, newPath); @@ -1581,6 +1610,7 @@ async function rename(oldPath, newPath) { } async function truncate(path, len = 0) { + if (shouldTraceFs('truncate')) return await traceFsPromise('truncate', truncate, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.truncate(path, len); @@ -1591,6 +1621,7 @@ async function truncate(path, len = 0) { } async function ftruncate(handle, len = 0) { + if (shouldTraceFs('ftruncate')) return await traceFsPromise('ftruncate', ftruncate, this, arguments); validateInteger(len, 'len'); len = MathMax(0, len); return await PromisePrototypeThen( @@ -1601,6 +1632,7 @@ async function ftruncate(handle, len = 0) { } async function rm(path, options) { + if (shouldTraceFs('rm')) return await traceFsPromise('rm', rm, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.rm(path, options); @@ -1612,6 +1644,7 @@ async function rm(path, options) { } async function rmdir(path, options) { + if (shouldTraceFs('rmdir')) return await traceFsPromise('rmdir', rmdir, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.rmdir(path); @@ -1637,6 +1670,7 @@ async function rmdir(path, options) { } async function fdatasync(handle) { + if (shouldTraceFs('fdatasync')) return await traceFsPromise('fdatasync', fdatasync, this, arguments); if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'); } @@ -1648,6 +1682,7 @@ async function fdatasync(handle) { } async function fsync(handle) { + if (shouldTraceFs('fsync')) return await traceFsPromise('fsync', fsync, this, arguments); if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'); } @@ -1659,6 +1694,7 @@ async function fsync(handle) { } async function mkdir(path, options) { + if (shouldTraceFs('mkdir')) return await traceFsPromise('mkdir', mkdir, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.mkdir(path, options); @@ -1726,6 +1762,7 @@ async function readdirRecursiveWithPermissionModel(basePath, options) { } async function readdir(path, options) { + if (shouldTraceFs('readdir')) return await traceFsPromise('readdir', readdir, this, arguments); options = getOptions(options); // Make shallow copy to prevent mutating options from affecting results @@ -1757,6 +1794,7 @@ async function readdir(path, options) { } async function readlink(path, options) { + if (shouldTraceFs('readlink')) return await traceFsPromise('readlink', readlink, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.readlink(path, options); @@ -1772,6 +1810,7 @@ async function readlink(path, options) { } async function symlink(target, path, type) { + if (shouldTraceFs('symlink')) return await traceFsPromise('symlink', symlink, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.symlink(target, path, type); @@ -1811,6 +1850,7 @@ async function symlink(target, path, type) { } async function fstat(handle, options = { __proto__: null, bigint: false }) { + if (shouldTraceFs('fstat')) return await traceFsPromise('fstat', fstat, this, arguments); validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1827,6 +1867,7 @@ async function fstat(handle, options = { __proto__: null, bigint: false }) { } async function lstat(path, options = { __proto__: null, bigint: false }) { + if (shouldTraceFs('lstat')) return await traceFsPromise('lstat', lstat, this, arguments); validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1854,6 +1895,7 @@ async function lstat(path, options = { __proto__: null, bigint: false }) { } async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { + if (shouldTraceFs('stat')) return await traceFsPromise('stat', stat, this, arguments); validateObject(options, 'options'); const { signal } = options; if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); @@ -1879,6 +1921,7 @@ async function stat(path, options = { __proto__: null, bigint: false, throwIfNoE } async function statfs(path, options = { __proto__: null, bigint: false }) { + if (shouldTraceFs('statfs')) return await traceFsPromise('statfs', statfs, this, arguments); const h = vfsState.handlers; if (h !== null) { const result = h.statfs(path, options); @@ -1894,6 +1937,7 @@ async function statfs(path, options = { __proto__: null, bigint: false }) { } async function link(existingPath, newPath) { + if (shouldTraceFs('link')) return await traceFsPromise('link', link, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.link(existingPath, newPath); @@ -1909,6 +1953,7 @@ async function link(existingPath, newPath) { } async function unlink(path) { + if (shouldTraceFs('unlink')) return await traceFsPromise('unlink', unlink, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.unlink(path); @@ -1922,6 +1967,7 @@ async function unlink(path) { } async function fchmod(handle, mode) { + if (shouldTraceFs('fchmod')) return await traceFsPromise('fchmod', fchmod, this, arguments); if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'); } @@ -1934,6 +1980,7 @@ async function fchmod(handle, mode) { } async function chmod(path, mode) { + if (shouldTraceFs('chmod')) return await traceFsPromise('chmod', chmod, this, arguments); path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); @@ -1951,6 +1998,7 @@ async function chmod(path, mode) { } async function lchmod(path, mode) { + if (shouldTraceFs('lchmod')) return await traceFsPromise('lchmod', lchmod, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.lchmod(path, mode); @@ -1965,6 +2013,7 @@ async function lchmod(path, mode) { } async function lchown(path, uid, gid) { + if (shouldTraceFs('lchown')) return await traceFsPromise('lchown', lchown, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.lchown(path, uid, gid); @@ -1982,6 +2031,7 @@ async function lchown(path, uid, gid) { } async function fchown(handle, uid, gid) { + if (shouldTraceFs('fchown')) return await traceFsPromise('fchown', fchown, this, arguments); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); if (permission.isEnabled()) { @@ -1995,6 +2045,7 @@ async function fchown(handle, uid, gid) { } async function chown(path, uid, gid) { + if (shouldTraceFs('chown')) return await traceFsPromise('chown', chown, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.chown(path, uid, gid); @@ -2012,6 +2063,7 @@ async function chown(path, uid, gid) { } async function utimes(path, atime, mtime) { + if (shouldTraceFs('utimes')) return await traceFsPromise('utimes', utimes, this, arguments); path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2030,6 +2082,7 @@ async function utimes(path, atime, mtime) { } async function futimes(handle, atime, mtime) { + if (shouldTraceFs('futimes')) return await traceFsPromise('futimes', futimes, this, arguments); if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'); } @@ -2043,6 +2096,7 @@ async function futimes(handle, atime, mtime) { } async function lutimes(path, atime, mtime) { + if (shouldTraceFs('lutimes')) return await traceFsPromise('lutimes', lutimes, this, arguments); path = getValidatedPath(path); atime = toUnixTimestamp(atime); mtime = toUnixTimestamp(mtime); @@ -2061,6 +2115,7 @@ async function lutimes(path, atime, mtime) { } async function realpath(path, options) { + if (shouldTraceFs('realpath')) return await traceFsPromise('realpath', realpath, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.realpath(path, options); @@ -2075,6 +2130,7 @@ async function realpath(path, options) { } async function mkdtemp(prefix, options) { + if (shouldTraceFs('mkdtemp')) return await traceFsPromise('mkdtemp', mkdtemp, this, arguments); const h = vfsState.handlers; if (h !== null) { const promise = h.mkdtemp(prefix, options); @@ -2095,6 +2151,9 @@ async function mkdtemp(prefix, options) { } async function mkdtempDisposable(prefix, options) { + if (shouldTraceFs('mkdtemp')) { + return await traceFsPromise('mkdtemp', mkdtempDisposable, this, arguments); + } options = getOptions(options); if (BufferIsBuffer(prefix)) { options = { ...options, encoding: 'buffer' }; @@ -2130,6 +2189,7 @@ async function mkdtempDisposable(prefix, options) { } async function writeFile(path, data, options) { + if (shouldTraceFs('writeFile')) return await traceFsPromise('writeFile', writeFile, this, arguments); options = getOptions(options, { encoding: 'utf8', mode: 0o666, @@ -2210,6 +2270,7 @@ function isCustomIterable(obj) { } async function appendFile(path, data, options) { + if (shouldTraceFs('appendFile')) return await traceFsPromise('appendFile', appendFile, this, arguments); options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); parseFileMode(options.mode, 'mode', 0o666); @@ -2225,6 +2286,7 @@ async function appendFile(path, data, options) { } async function readFile(path, options) { + if (shouldTraceFs('readFile')) return await traceFsPromise('readFile', readFile, this, arguments); const h = vfsState.handlers; if (h !== null) { checkAborted(options?.signal); @@ -2283,6 +2345,14 @@ function readFileInOneRoundTrip(path, flagsNumber) { }); } +const opendirPromisified = promisify(opendirCallback); +// The unused `callback` keeps the length of promisify(fs.opendir). +function opendir(path, options, callback) { + if (shouldTraceFs('opendir')) return traceFsPromise('opendir', opendir, this, arguments); + // Skips the check in fs.opendir(), so the call is traced only once. + return callUntraced(opendirPromisified, undefined, [path, options]); +} + async function* _watch(filename, options = kEmptyObject) { const h = vfsState.handlers; if (h !== null) { @@ -2324,7 +2394,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 3aadc19c4732..83c8cbaa26b3 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -3,6 +3,8 @@ const { ArrayIsArray, ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeSlice, BigInt, Date, DateNow, @@ -17,6 +19,7 @@ const { ObjectDefineProperties, ObjectIs, ObjectSetPrototypeOf, + ReflectApply, ReflectOwnKeys, RegExpPrototypeSymbolReplace, StringPrototypeEndsWith, @@ -1191,6 +1194,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 (shouldTraceFs('name')) return traceFsSync('name', fn, this, arguments); +// +// The trace helpers call the same function again inside the trace, so +// that its whole body runs inside the `start` scope. They set this flag +// just before that call, so that the call skips the check once. +let skipNextTrace = false; + +function shouldTraceFs(name) { + if (skipNextTrace) { + skipNextTrace = false; + return false; + } + return getFsChannel(name).hasSubscribers; +} + +function callUntraced(fn, thisArg, args) { + skipNextTrace = true; + try { + return ReflectApply(fn, thisArg, args); + } finally { + skipNextTrace = false; + } +} + +let tracingChannel; +const fsChannels = { __proto__: null }; +function getFsChannel(name) { + let channel = fsChannels[name]; + if (channel === undefined) { + tracingChannel ??= require('diagnostics_channel').tracingChannel; + channel = fsChannels[name] = tracingChannel(`fs.${name}`); + } + return channel; +} + +// Context property names for the leading arguments of each operation. +// All arguments are also in `context.args`. +const kFsContextFields = { + __proto__: null, + access: ['path'], + appendFile: ['path', 'data'], + chmod: ['path', 'mode'], + chown: ['path', 'uid', 'gid'], + close: ['fd'], + copyFile: ['path', 'dest'], + cp: ['path', 'dest'], + exists: ['path'], + fchmod: ['fd', 'mode'], + fchown: ['fd', 'uid', 'gid'], + fdatasync: ['fd'], + fstat: ['fd'], + fsync: ['fd'], + ftruncate: ['fd'], + futimes: ['fd', 'atime', 'mtime'], + glob: ['pattern'], + lchmod: ['path', 'mode'], + lchown: ['path', 'uid', 'gid'], + link: ['path', 'dest'], + lstat: ['path'], + lutimes: ['path', 'atime', 'mtime'], + mkdir: ['path'], + mkdtemp: ['prefix'], + open: ['path'], + openAsBlob: ['path'], + opendir: ['path'], + read: ['fd'], + readdir: ['path'], + readFile: ['path'], + readlink: ['path'], + readv: ['fd', 'buffers'], + realpath: ['path'], + rename: ['path', 'dest'], + rm: ['path'], + rmdir: ['path'], + stat: ['path'], + statfs: ['path'], + symlink: ['target', 'path'], + truncate: ['path'], + unlink: ['path'], + utimes: ['path', 'atime', 'mtime'], + write: ['fd', 'buffer'], + writeFile: ['path', 'data'], + writev: ['fd', 'buffers'], +}; + +// `args` are the caller's arguments. `context.args` is a copy, so that +// subscribers can not change the arguments of the call. +function createFsContext(name, api, args) { + const fields = kFsContextFields[name]; + let start = 0; + // FileHandle methods pass the handle in place of a descriptor. It is + // not one of the arguments of the method. + if (fields[0] === 'fd' && typeof args[0] === 'object' && args[0] !== null) { + start = 1; + } + const context = { api, args: ArrayPrototypeSlice(args, start) }; + for (let i = 0; i < fields.length && i < args.length; i++) { + context[fields[i]] = i === 0 && start === 1 ? args[0].fd : args[i]; + } + return context; +} + +function traceFsSync(name, fn, thisArg, args) { + const context = createFsContext(name, 'sync', args); + return getFsChannel(name).traceSync(callUntraced, context, undefined, + fn, thisArg, args); +} + +function traceFsPromise(name, fn, thisArg, args) { + const context = createFsContext(name, 'promise', args); + return getFsChannel(name).tracePromise(callUntraced, context, undefined, + fn, thisArg, args); +} + +function callUntracedSpread(fn, thisArg, ...args) { + return callUntraced(fn, thisArg, args); +} + +// The callback is the last function argument. Without one, the +// function throws when it is called again. `contextArgs` are the +// arguments to report, when they differ from the arguments of the call. +function traceFsCallback(name, fn, thisArg, args, contextArgs = args) { + let position = args.length - 1; + while (position >= 0 && typeof args[position] !== 'function') position--; + if (position < 0) return callUntraced(fn, thisArg, args); + const context = createFsContext(name, 'callback', contextArgs); + const callArgs = [callUntracedSpread, position + 2, context, undefined, fn, thisArg]; + ArrayPrototypePushApply(callArgs, args); + const channel = getFsChannel(name); + return ReflectApply(channel.traceCallback, channel, callArgs); +} + module.exports = { collectRecursiveReaddirResult, constants: { @@ -1209,11 +1346,17 @@ module.exports = { DirentFromStats, getDirent, getDirents, + getFsChannel, getOptions, getRecursiveDirents, getValidatedFd, getValidatedPath, handleErrorFromBinding, + callUntraced, + shouldTraceFs, + traceFsCallback, + traceFsPromise, + traceFsSync, 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..376ea04e5d6e --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-callback.js @@ -0,0 +1,164 @@ +'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'); + 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..746d4391b5f4 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-coverage.js @@ -0,0 +1,330 @@ +'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()], + ['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)], + ['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()], + ['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..72fc60157f6c --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs-sync.js @@ -0,0 +1,153 @@ +'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); +} + +{ + 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(); +}