diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 847995f1807d6e..69a583437d5205 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -89,6 +89,7 @@ const { const { EventEmitterMixin } = require('internal/event_target'); const { StringDecoder } = require('string_decoder'); const { watch } = require('internal/fs/watchers'); +const linuxWatcher = require('internal/fs/watch/linux'); const { isIterable } = require('internal/streams/utils'); const assert = require('internal/assert'); @@ -120,6 +121,7 @@ const getDirectoryEntriesPromise = promisify(getDirents); const validateRmOptionsPromise = promisify(validateRmOptions); const isWindows = process.platform === 'win32'; +const isLinux = process.platform === 'linux'; let cpPromises; function lazyLoadCpPromises() { @@ -917,6 +919,21 @@ async function readFile(path, options) { return handleFdClose(readFileHandle(fd, options), fd.close); } +function _watch(filename, options) { + options = getOptions(options, { persistent: true, recursive: false, encoding: 'utf8' }); + + // TODO(anonrig): Remove this when/if libuv supports it. + // libuv does not support recursive file watch on Linux due to + // the limitations of inotify. + if (options.recursive && isLinux) { + const watcher = new linuxWatcher.FSWatcher(options); + watcher[linuxWatcher.kFSWatchStart](filename); + return watcher; + } + + return watch(filename, options); +} + module.exports = { exports: { access, @@ -947,7 +964,7 @@ module.exports = { writeFile, appendFile, readFile, - watch, + watch: _watch, constants, }, diff --git a/lib/internal/fs/watch/linux.js b/lib/internal/fs/watch/linux.js index e1c9faa6f9473f..55c0e436f5a929 100644 --- a/lib/internal/fs/watch/linux.js +++ b/lib/internal/fs/watch/linux.js @@ -1,11 +1,12 @@ 'use strict'; +const { SafeMap, Symbol, StringPrototypeStartsWith, SymbolAsyncIterator, Promise } = primordials; + +const { ERR_FEATURE_UNAVAILABLE_ON_PLATFORM } = require('internal/errors'); +const { kEmptyObject } = require('internal/util'); +const { validateObject, validateString } = require('internal/validators'); const { EventEmitter } = require('events'); const path = require('path'); -const { SafeMap, Symbol, StringPrototypeStartsWith } = primordials; -const { validateObject, validateString } = require('internal/validators'); -const { kEmptyObject } = require('internal/util'); -const { ERR_FEATURE_UNAVAILABLE_ON_PLATFORM } = require('internal/errors'); const kFSWatchStart = Symbol('kFSWatchStart'); @@ -185,6 +186,25 @@ class FSWatcher extends EventEmitter { // This is kept to have the same API with FSWatcher throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('unref'); } + + async next() { + if (this.#closed) { + return { done: true }; + } + + const result = await new Promise((resolve) => { + this.once('change', (eventType, filename) => { + resolve({ eventType, filename }); + }); + }); + + return { done: false, value: result }; + } + + // eslint-disable-next-line require-yield + *[SymbolAsyncIterator]() { + return this; + } } module.exports = { diff --git a/test/parallel/test-fs-watch-recursive-linux-promise.js b/test/parallel/test-fs-watch-recursive-linux-promise.js new file mode 100644 index 00000000000000..6eb43b34c85776 --- /dev/null +++ b/test/parallel/test-fs-watch-recursive-linux-promise.js @@ -0,0 +1,40 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { randomUUID } = require('crypto'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs/promises'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + { + // Add a file to already watching folder + + const testsubdir = await fs.mkdtemp(testDir + path.sep); + const file = `${randomUUID()}.txt`; + const filePath = path.join(testsubdir, file); + const watcher = fs.watch(testsubdir, { recursive: true }); + + setTimeout(async () => { + await fs.writeFile(filePath, 'world'); + }, 100); + + for await (const payload of watcher) { + const { eventType, filename } = payload; + + assert.ok(eventType === 'change' || eventType === 'rename'); + + if (filename === file) { + break; + } + } + } +})().then(common.mustCall());