Skip to content

Commit 50074a2

Browse files
committed
switch from chokidar to fs.watchFile (built into nodejs) for watching a *single* file, due to chokidar being so buggy in polling mode
1 parent fb116e1 commit 50074a2

File tree

1 file changed

+37
-46
lines changed

1 file changed

+37
-46
lines changed

src/packages/backend/watcher.ts

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
/*
77
Watch one SINGLE FILE for changes. Use ./path-watcher.ts for a directory.
88
9-
Watch for changes to the given file. Returns obj, which
9+
Watch for changes to the given file, which means the mtime changes or the
10+
mode changes (e.g., readonly versus readwrite). Returns obj, which
1011
is an event emitter with events:
1112
1213
- 'change', ctime, stats - when file changes or is created
@@ -15,21 +16,27 @@ is an event emitter with events:
1516
and a method .close().
1617
1718
Only fires after the file definitely has not had its
18-
ctime changed for at least debounce ms (this is the atomic
19-
option to chokidar). Does NOT fire when the file first
20-
has ctime changed.
19+
ctime changed for at least debounce ms. Does NOT
20+
fire when the file first has ctime changed.
21+
22+
NOTE: for directories we use chokidar in path-watcher. However,
23+
for a single file using polling, chokidar is horribly buggy and
24+
lacking in functionality (e.g., https://github.com/paulmillr/chokidar/issues/1132),
25+
and declared all bugs fixed, so we steer clear. It had a lot of issues
26+
with just noticing actual file changes.
27+
28+
We *always* use polling to fully support networked filesystems.
2129
*/
2230

2331
import { EventEmitter } from "node:events";
24-
import { watch, FSWatcher } from "chokidar";
32+
import { unwatchFile, watchFile } from "node:fs";
2533
import { getLogger } from "./logger";
2634
import { debounce as lodashDebounce } from "lodash";
2735

2836
const logger = getLogger("backend:watcher");
2937

3038
export class Watcher extends EventEmitter {
3139
private path: string;
32-
private watcher: FSWatcher;
3340

3441
constructor(
3542
path: string,
@@ -38,49 +45,33 @@ export class Watcher extends EventEmitter {
3845
super();
3946
this.path = path;
4047

41-
logger.debug({ path, debounce, interval });
42-
this.watcher = watch(this.path, {
43-
interval,
44-
// polling is critical for network mounted file systems,
45-
// and given architecture of cocalc there is no easy way around this.
46-
// E.g., on compute servers, everything breaks involving sync or cloudfs,
47-
// and in shared project s3/gcsfuse/sshfs would all break. So we
48-
// use polling.
49-
usePolling: true,
50-
persistent: true,
51-
alwaysStat: true,
52-
});
53-
this.watcher.on("unlink", () => {
54-
this.emit("delete");
55-
});
56-
this.watcher.on("unlinkDir", () => {
57-
this.emit("delete");
58-
});
59-
60-
const f = (ctime, stats) => {
61-
logger.debug("change", this.path, ctime);
62-
this.emit("change", ctime, stats);
63-
};
64-
const emitChange = debounce ? lodashDebounce(f, debounce) : f;
65-
66-
this.watcher.on("error", (err) => {
67-
logger.debug("WATCHER error -- ", err);
68-
});
69-
70-
this.watcher.on("change", (_, stats) => {
71-
if (stats == null) {
72-
logger.debug("WATCHER change with no stats (shouldn't happen)", {
73-
path,
74-
});
75-
return;
76-
}
77-
emitChange(stats.ctime, stats);
78-
});
48+
logger.debug("watchFile", { path, debounce, interval });
49+
watchFile(this.path, { persistent: false, interval }, this.handleChange);
50+
51+
if (debounce) {
52+
this.emitChange = lodashDebounce(this.emitChange, debounce);
53+
}
7954
}
8055

81-
close = async () => {
56+
private emitChange = (stats) => {
57+
this.emit("change", stats.ctime, stats);
58+
};
59+
60+
private handleChange = (curr, prev) => {
61+
if (!curr.dev) {
62+
this.emit("delete");
63+
return;
64+
}
65+
if (curr.mtimeMs == prev.mtimeMs && curr.mode == prev.mode) {
66+
// just *accessing* triggers watchFile (really StatWatcher), of course.
67+
return;
68+
}
69+
this.emitChange(curr);
70+
};
71+
72+
close = () => {
8273
logger.debug("close", this.path);
8374
this.removeAllListeners();
84-
await this.watcher.close();
75+
unwatchFile(this.path, this.handleChange);
8576
};
8677
}

0 commit comments

Comments
 (0)