From 1c6772a42dc1c182b4cb84753146f7e1bc1eb2d8 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Fri, 18 Nov 2022 00:26:18 +0200 Subject: [PATCH 1/2] fs: fix `nonNativeWatcher` leak of `StatWatchers` PR-URL: https://github.com/nodejs/node/pull/45501 Reviewed-By: Yagiz Nizipli --- lib/internal/fs/recursive_watch.js | 9 +++----- test/parallel/test-fs-watch-recursive.js | 27 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/internal/fs/recursive_watch.js b/lib/internal/fs/recursive_watch.js index ed146fa28bed8d..5c7a3e5b39a9a2 100644 --- a/lib/internal/fs/recursive_watch.js +++ b/lib/internal/fs/recursive_watch.js @@ -154,14 +154,11 @@ class FSWatcher extends EventEmitter { this.#symbolicFiles.add(f); } + this.#files.set(f, file); if (file.isFile()) { this.#watchFile(f); - } else { - this.#files.set(f, file); - - if (file.isDirectory() && !file.isSymbolicLink()) { - await this.#watchFolder(f); - } + } else if (file.isDirectory() && !file.isSymbolicLink()) { + await this.#watchFolder(f); } } } diff --git a/test/parallel/test-fs-watch-recursive.js b/test/parallel/test-fs-watch-recursive.js index 70b413814e78f4..acf3a3bc6a421d 100644 --- a/test/parallel/test-fs-watch-recursive.js +++ b/test/parallel/test-fs-watch-recursive.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common'); +const { setTimeout } = require('timers/promises'); const assert = require('assert'); @@ -52,3 +53,29 @@ if (common.isOSX) { process.on('exit', function() { assert(watcherClosed, 'watcher Object was not closed'); }); + +(async () => { + // Assert recursive watch does not leak handles + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + const testDirectory = path.join(rootDirectory, 'test-7'); + const filePath = path.join(testDirectory, 'only-file.txt'); + fs.mkdirSync(testDirectory); + + let watcherClosed = false; + const watcher = fs.watch(testDirectory, { recursive: true }); + watcher.on('change', common.mustCallAtLeast(async (event, filename) => { + await setTimeout(common.platformTimeout(100)); + if (filename === path.basename(filePath)) { + watcher.close(); + watcherClosed = true; + } + await setTimeout(common.platformTimeout(100)); + assert(!process._getActiveHandles().some((handle) => handle.constructor.name === 'StatWatcher')); + })); + + process.on('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(filePath, 'content'); +})().then(common.mustCall()); From 551a92d0c2c27e7e1ae9e0dc4d8846af41b09970 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Mon, 28 Nov 2022 00:03:42 +0200 Subject: [PATCH 2/2] fs: fix `nonNativeWatcher` watching folder with existing files PR-URL: https://github.com/nodejs/node/pull/45500 Reviewed-By: Yagiz Nizipli --- lib/internal/fs/recursive_watch.js | 3 +++ test/parallel/test-fs-watch-recursive.js | 30 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/internal/fs/recursive_watch.js b/lib/internal/fs/recursive_watch.js index 5c7a3e5b39a9a2..cbe513a166ec20 100644 --- a/lib/internal/fs/recursive_watch.js +++ b/lib/internal/fs/recursive_watch.js @@ -199,6 +199,9 @@ class FSWatcher extends EventEmitter { this.emit('change', 'rename', pathRelative(this.#rootPath, file)); } else if (currentStats.isDirectory()) { this.#watchFolder(file); + } else { + // Watching a directory will trigger a change event for child files) + this.emit('change', 'change', pathRelative(this.#rootPath, file)); } }); } diff --git a/test/parallel/test-fs-watch-recursive.js b/test/parallel/test-fs-watch-recursive.js index acf3a3bc6a421d..eb91860d2e66ac 100644 --- a/test/parallel/test-fs-watch-recursive.js +++ b/test/parallel/test-fs-watch-recursive.js @@ -54,6 +54,36 @@ process.on('exit', function() { assert(watcherClosed, 'watcher Object was not closed'); }); +(async () => { + // Watch a folder and update an already existing file in it. + + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + const testDirectory = path.join(rootDirectory, 'test-0'); + fs.mkdirSync(testDirectory); + + const testFile = path.join(testDirectory, 'file-1.txt'); + fs.writeFileSync(testFile, 'hello'); + + const watcher = fs.watch(testDirectory, { recursive: true }); + let watcherClosed = false; + watcher.on('change', common.mustCallAtLeast(function(event, filename) { + // Libuv inconsistenly emits a rename event for the file we are watching + assert.ok(event === 'change' || event === 'rename'); + + if (filename === path.basename(testFile)) { + watcher.close(); + watcherClosed = true; + } + })); + + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(testFile, 'hello'); + + process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); +})().then(common.mustCall()); + (async () => { // Assert recursive watch does not leak handles const rootDirectory = fs.mkdtempSync(testDir + path.sep);