From 409ef510892e46046d75c95ea6655376a76706d9 Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:21:36 -0700 Subject: [PATCH 1/7] Follow symlinks Solves #231 and updates the README to match. --- README.md | 1 - lib/DirectoryWatcher.js | 23 +++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2924722..c84e314 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ watchpack high level API doesn't map directly to watchers. Instead a three level - The real watchers are created by the `DirectoryWatcher`. - Files are never watched directly. This should keep the watcher count low. - Watching can be started in the past. This way watching can start after file reading. -- Symlinks are not followed, instead the symlink is watched. ## API diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index abbb142..4181747 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -392,7 +392,8 @@ class DirectoryWatcher extends EventEmitter { const checkStats = () => { if (this.closed) return; this._activeEvents.set(filename, false); - fs.lstat(filePath, (err, stats) => { + + const handleStats = (err, stats) => { if (this.closed) return; if (this._activeEvents.get(filename) === true) { process.nextTick(checkStats); @@ -438,8 +439,15 @@ class DirectoryWatcher extends EventEmitter { false, eventType ); + if ( + this.watcherManager.options.followSymlinks && + stats.isSymbolicLink() + ) { + fs.stat(filePath, handleStats); + } } - }); + }; + fs.lstat(filePath, handleStats); }; process.nextTick(checkStats); } else { @@ -625,7 +633,7 @@ class DirectoryWatcher extends EventEmitter { } }); for (const itemPath of itemPaths) { - fs.lstat(itemPath, (err2, stats) => { + const handleStats = (err2, stats) => { if (this.closed) return; if (err2) { if ( @@ -652,6 +660,12 @@ class DirectoryWatcher extends EventEmitter { true, "scan (file)" ); + if ( + this.watcherManager.options.followSymlinks && + stats.isSymbolicLink() + ) { + fs.stat(itemPath, handleStats); + } } else if (stats.isDirectory()) { if (!initial || !this.directories.has(itemPath)) this.setDirectory( @@ -662,7 +676,8 @@ class DirectoryWatcher extends EventEmitter { ); } itemFinished(); - }); + }; + fs.lstat(itemPath, handleStats); } itemFinished(); }); From 289ce5aed7c525a7e859497d019a66d686f760ce Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Thu, 7 Mar 2024 22:04:56 -0700 Subject: [PATCH 2/7] Added test to detect symlinked file change outside watched directory --- test/Watchpack.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/Watchpack.js b/test/Watchpack.js index 4559c62..a1963ba 100644 --- a/test/Watchpack.js +++ b/test/Watchpack.js @@ -1237,6 +1237,8 @@ describe("Watchpack", function() { testHelper.symlinkFile(path.join("a", "b", "link"), "c"); testHelper.symlinkFile(path.join("a", "b", "link2"), "link"); testHelper.symlinkFile("link2", "link/link/link2"); + testHelper.dir("b"); + testHelper.symlinkDir(path.join("b", "link"), path.join("..", "a", "b")); testHelper.tick(1000, done); }); @@ -1368,6 +1370,20 @@ describe("Watchpack", function() { } ); }); + + it("should detect a change to symlinked file outside watched directory", function(done) { + expectWatchEvent( + [], + path.join(fixtures, "b"), + changes => { + Array.from(changes).should.be.eql([path.join(fixtures, "b")]); + done(); + }, + () => { + testHelper.file(path.join("a", "b", "d")); + } + ); + }); }); } else { it("symlinks"); From 5b221010f2e30e308423d32cb77c399a6ef4f4ea Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Sat, 9 Mar 2024 16:05:59 -0700 Subject: [PATCH 3/7] Using both lstat and stat in onWatchEvent was unnecessary --- lib/DirectoryWatcher.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index 4181747..104f40a 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -393,7 +393,7 @@ class DirectoryWatcher extends EventEmitter { if (this.closed) return; this._activeEvents.set(filename, false); - const handleStats = (err, stats) => { + fs.lstat(filePath, (err, stats) => { if (this.closed) return; if (this._activeEvents.get(filename) === true) { process.nextTick(checkStats); @@ -439,15 +439,8 @@ class DirectoryWatcher extends EventEmitter { false, eventType ); - if ( - this.watcherManager.options.followSymlinks && - stats.isSymbolicLink() - ) { - fs.stat(filePath, handleStats); - } } - }; - fs.lstat(filePath, handleStats); + }); }; process.nextTick(checkStats); } else { From c54fa9f73b2db6b164bd812baadcacfd4e87d046 Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:11:46 -0700 Subject: [PATCH 4/7] calling both lstat and stat isnt needed in doScan --- lib/DirectoryWatcher.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index 104f40a..b2bc39f 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -653,12 +653,6 @@ class DirectoryWatcher extends EventEmitter { true, "scan (file)" ); - if ( - this.watcherManager.options.followSymlinks && - stats.isSymbolicLink() - ) { - fs.stat(itemPath, handleStats); - } } else if (stats.isDirectory()) { if (!initial || !this.directories.has(itemPath)) this.setDirectory( @@ -670,7 +664,11 @@ class DirectoryWatcher extends EventEmitter { } itemFinished(); }; - fs.lstat(itemPath, handleStats); + if (this.watcherManager.options.followSymlinks) { + fs.stat(itemPath, handleStats); + } else { + fs.lstat(itemPath, handleStats); + } } itemFinished(); }); From 0cd204be3996ce1e173f33664ac70820522f5fbd Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:19:26 -0600 Subject: [PATCH 5/7] wondering if its just slow --- test/Watchpack.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Watchpack.js b/test/Watchpack.js index a1963ba..7f53c1b 100644 --- a/test/Watchpack.js +++ b/test/Watchpack.js @@ -1226,7 +1226,9 @@ describe("Watchpack", function() { } if (symlinksSupported) { - describe("symlinks", () => { + describe("symlinks", function() { + this.timeout(20000); + beforeEach(done => { testHelper.dir("a"); testHelper.dir(path.join("a", "b")); From c54bf67639ab35aa99fc561387b0266ae2965ed0 Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:58:01 -0600 Subject: [PATCH 6/7] Update Watchpack.js --- test/Watchpack.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Watchpack.js b/test/Watchpack.js index 7f53c1b..a1963ba 100644 --- a/test/Watchpack.js +++ b/test/Watchpack.js @@ -1226,9 +1226,7 @@ describe("Watchpack", function() { } if (symlinksSupported) { - describe("symlinks", function() { - this.timeout(20000); - + describe("symlinks", () => { beforeEach(done => { testHelper.dir("a"); testHelper.dir(path.join("a", "b")); From d8e305f1135c14e101cbc11b5824bd048a4d0f9d Mon Sep 17 00:00:00 2001 From: ArjhanToteck <38510221+ArjhanToteck@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:42:42 -0600 Subject: [PATCH 7/7] Fixed for polling servers It should now be able to watch symlinks that lead to outside of the watched folder when polling --- lib/DirectoryWatcher.js | 74 +++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index 54788ea..efcc953 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -1,4 +1,4 @@ -/* +/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ @@ -61,6 +61,7 @@ class DirectoryWatcher extends EventEmitter { this.watcherManager = watcherManager; this.options = options; this.path = directoryPath; + this.watchingSymlink = false; // safeTime is the point in time after which reading is safe to be unchanged // timestamp is a value that should be compared with another timestamp (mtime) /** @type {Map { if (this.closed) return; if (err) { - if (err.code === "ENOENT" || err.code === "EPERM") { - this.onDirectoryRemoved("scan readdir failed"); - } else { - this.onScanError(err); - } this.initialScan = false; this.initialScanFinished = Date.now(); if (initial) { @@ -627,22 +623,7 @@ class DirectoryWatcher extends EventEmitter { } }); for (const itemPath of itemPaths) { - const handleStats = (err2, stats) => { - if (this.closed) return; - if (err2) { - if ( - err2.code === "ENOENT" || - err2.code === "EPERM" || - err2.code === "EACCES" || - err2.code === "EBUSY" - ) { - this.setMissing(itemPath, initial, "scan (" + err2.code + ")"); - } else { - this.onScanError(err2); - } - itemFinished(); - return; - } + const handleStats = (stats, symlinkStats) => { if (stats.isFile() || stats.isSymbolicLink()) { if (stats.mtime) { ensureFsAccuracy(stats.mtime); @@ -654,7 +635,11 @@ class DirectoryWatcher extends EventEmitter { true, "scan (file)" ); - } else if (stats.isDirectory()) { + } + if ( + stats.isDirectory() || + (symlinkStats && symlinkStats.isDirectory()) + ) { if (!initial || !this.directories.has(itemPath)) this.setDirectory( itemPath, @@ -665,11 +650,42 @@ class DirectoryWatcher extends EventEmitter { } itemFinished(); }; - if (this.watcherManager.options.followSymlinks) { - fs.stat(itemPath, handleStats); - } else { - fs.lstat(itemPath, handleStats); - } + fs.lstat(itemPath, (err2, stats) => { + if (this.closed) return; + if (err2) { + if ( + err2.code === "ENOENT" || + err2.code === "EPERM" || + err2.code === "EACCES" || + err2.code === "EBUSY" + ) { + this.setMissing(itemPath, initial, "scan (" + err2.code + ")"); + } else { + this.onScanError(err2); + } + itemFinished(); + return; + } + if ( + stats.isSymbolicLink() && + this.watcherManager.options.followSymlinks + ) { + fs.stat(itemPath, (err3, symlinkStats) => { + if (this.closed) return; + // something is wrong with the symlink, but not with the file itself + if (err3) { + handleStats(stats); + this.watchingSymlink = false; + return; + } + this.watchingSymlink = true; + handleStats(stats, symlinkStats); + }); + } else { + this.watchingSymlink = false; + handleStats(stats); + } + }); } itemFinished(); });