6
6
/*
7
7
Watch one SINGLE FILE for changes. Use ./path-watcher.ts for a directory.
8
8
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
10
11
is an event emitter with events:
11
12
12
13
- 'change', ctime, stats - when file changes or is created
@@ -15,21 +16,27 @@ is an event emitter with events:
15
16
and a method .close().
16
17
17
18
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.
21
29
*/
22
30
23
31
import { EventEmitter } from "node:events" ;
24
- import { watch , FSWatcher } from "chokidar " ;
32
+ import { unwatchFile , watchFile } from "node:fs " ;
25
33
import { getLogger } from "./logger" ;
26
34
import { debounce as lodashDebounce } from "lodash" ;
27
35
28
36
const logger = getLogger ( "backend:watcher" ) ;
29
37
30
38
export class Watcher extends EventEmitter {
31
39
private path : string ;
32
- private watcher : FSWatcher ;
33
40
34
41
constructor (
35
42
path : string ,
@@ -38,49 +45,33 @@ export class Watcher extends EventEmitter {
38
45
super ( ) ;
39
46
this . path = path ;
40
47
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
+ }
79
54
}
80
55
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 = ( ) => {
82
73
logger . debug ( "close" , this . path ) ;
83
74
this . removeAllListeners ( ) ;
84
- await this . watcher . close ( ) ;
75
+ unwatchFile ( this . path , this . handleChange ) ;
85
76
} ;
86
77
}
0 commit comments