Version
20.14.0
Platform
Linux fedora 6.8.11-300.fc40.x86_64 #1 SMP PREEMPT_DYNAMIC Mon May 27 14:53:33 UTC 2024 x86_64 GNU/Linux
Subsystem
internal/fs/recursive_watch
What steps will reproduce the bug?
- Create a folder with
watch.mjs:
import { watch } from "node:fs";
const watcher = watch(import.meta.dirname, { recursive: true });
watcher.on("change", (eventType, filename) => {
// console.log(eventType, filename);
});
watcher.on("error", (e) => {
console.error(e);
});
console.log("Watching for changes...");
git init in folder
node watch.mjs
- in the same folder, run the following:
for run in {1..100}; do git add watch.mjs && git rm --cached watch.mjs; done
the above command stages and unstages watch.mjs 100 times. you should see watch errors
Error: ENOENT: no such file or directory, stat '/home/avi/projects/watch-crash/.git/index.lock'
at statSync (node:fs:1658:25)
at #watchFile (node:internal/fs/recursive_watch:152:28)
at #watchFolder (node:internal/fs/recursive_watch:129:26)
at FSWatcher.<anonymous> (node:internal/fs/recursive_watch:184:26)
at FSWatcher.emit (node:events:519:28)
at FSWatcher._handle.onchange (node:internal/fs/watchers:215:12)
and
Error: ENOENT: no such file or directory, watch '/home/avi/projects/watch-crash/.git/index.lock'
at FSWatcher.<computed> (node:internal/fs/watchers:247:19)
at watch (node:fs:2469:36)
at #watchFile (node:internal/fs/recursive_watch:156:21)
at #watchFolder (node:internal/fs/recursive_watch:129:26)
at FSWatcher.<anonymous> (node:internal/fs/recursive_watch:184:26)
at FSWatcher.emit (node:events:519:28)
at FSWatcher._handle.onchange (node:internal/fs/watchers:215:12)
This is because git saves the lock, and removes it immediately.
Sometimes this statSync call fails (recursive_watch:152):
{
const existingStat = statSync(file);
this.#files.set(file, existingStat);
}
which could have been avoided by using throwIfNoEntry: false and checking the return value.
the real race happens when the above statSync call succeeds, and then the watch() call (two lines down) fails because the file just got deleted. This can be seen in the second stack trace above. In this scenario, this.#files will be populated with the stats object, but this.#watchers won't have a matching watcher registered in the map.
When calling close() on watcher after the second scenario happened, the following loop in close will fail:
for (const file of this.#files.keys()) {
this.#watchers.get(file).close();
this.#watchers.delete(file);
}
as this.#watchers.get(file).close(); will fail when trying to call close() on undefined (watch() call blew up, so nothing in this.#watchers for file).
the error looks like:
TypeError: Cannot read properties of undefined (reading 'close')
at FSWatcher.close (node:internal/fs/recursive_watch:86:31)
How often does it reproduce? Is there a required condition?
Pretty consistently.
What is the expected behavior? Why is that the expected behavior?
Don't crash on close().
When querying whether a node exists or not (statSync), use throwIfNoEntry to avoid generated errors.
What do you see instead?
calling close() crashes the process
Additional information
@mcollina you might be interested. looks like your cup of tea.
Version
20.14.0
Platform
Linux fedora 6.8.11-300.fc40.x86_64 #1 SMP PREEMPT_DYNAMIC Mon May 27 14:53:33 UTC 2024 x86_64 GNU/Linux
Subsystem
internal/fs/recursive_watch
What steps will reproduce the bug?
watch.mjs:git initin foldernode watch.mjsthe above command stages and unstages
watch.mjs100 times. you should see watch errorsand
This is because git saves the lock, and removes it immediately.
Sometimes this
statSynccall fails (recursive_watch:152):which could have been avoided by using
throwIfNoEntry: falseand checking the return value.the real race happens when the above
statSynccall succeeds, and then thewatch()call (two lines down) fails because the file just got deleted. This can be seen in the second stack trace above. In this scenario,this.#fileswill be populated with the stats object, butthis.#watcherswon't have a matching watcher registered in the map.When calling
close()on watcher after the second scenario happened, the following loop in close will fail:as
this.#watchers.get(file).close();will fail when trying to callclose()on undefined (watch()call blew up, so nothing inthis.#watchersforfile).the error looks like:
How often does it reproduce? Is there a required condition?
Pretty consistently.
What is the expected behavior? Why is that the expected behavior?
Don't crash on
close().When querying whether a node exists or not (
statSync), usethrowIfNoEntryto avoid generated errors.What do you see instead?
calling
close()crashes the processAdditional information
@mcollina you might be interested. looks like your cup of tea.