Skip to content

Commit 836be21

Browse files
committed
Implement --polling option; poll files to detect changes instead of fsnotify
2 parents c57c1dd + 9076e07 commit 836be21

File tree

8 files changed

+654
-29
lines changed

8 files changed

+654
-29
lines changed

README.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,17 @@ Usage: arelo [OPTION]... -- COMMAND
4545
Run the COMMAND and restart when a file matches the pattern has been modified.
4646
4747
Options:
48-
-d, --delay duration duration to delay the restart of the command (default 1s)
49-
-f, --filter event filter file system event (CREATE|WRITE|REMOVE|RENAME|CHMOD)
50-
-h, --help display this message
51-
-i, --ignore glob ignore pathname glob pattern
52-
-p, --pattern glob trigger pathname glob pattern (default "**")
53-
-r, --restart restart the command on exit
54-
-s, --signal signal signal used to stop the command (default "SIGTERM")
55-
-t, --target path observation target path (default "./")
56-
-v, --verbose verbose output
57-
-V, --version display version
48+
-d, --delay duration duration to delay the restart of the command (default 1s)
49+
-f, --filter event filter file system event (CREATE|WRITE|REMOVE|RENAME|CHMOD)
50+
-h, --help display this message
51+
-i, --ignore glob ignore pathname glob pattern
52+
-p, --pattern glob trigger pathname glob pattern (default "**")
53+
--polling interval poll files at given interval instead of using fsnotify
54+
-r, --restart restart the command on exit
55+
-s, --signal signal signal used to stop the command (default "SIGTERM")
56+
-t, --target path observation target path (default "./")
57+
-v, --verbose verbose output
58+
-V, --version display version
5859
```
5960

6061
### Options
@@ -121,6 +122,15 @@ This option is not available on Windows.
121122

122123
Automatically restart the command when it exits, similar to when the pattern matched file is modified.
123124

125+
#### --polling interval
126+
127+
Poll files at the specified interval instead of using fsnotify.
128+
If not set or set to `0`, fsnotify is used for file monitoring.
129+
130+
This option is useful when fsnotify cannot detect changes, such as on WSL2.
131+
132+
The interval is specified as a number with a unit suffix ("ns", "us" (or "µs"), "ms", "s", "m", "h").
133+
124134
#### -v, --verbose
125135

126136
Output logs verbosely.

arelo.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"fmt"
77
"io"
8-
"io/fs"
98
"log"
109
"os"
1110
"os/signal"
@@ -21,6 +20,8 @@ import (
2120
"github.com/fsnotify/fsnotify"
2221
"github.com/spf13/pflag"
2322
"golang.org/x/xerrors"
23+
24+
"github.com/makiuchi-d/arelo/fspoll"
2425
)
2526

2627
const (
@@ -44,6 +45,7 @@ Options:
4445
help = pflag.BoolP("help", "h", false, "display this message")
4546
showver = pflag.BoolP("version", "V", false, "display version")
4647
filters = pflag.StringArrayP("filter", "f", nil, "filter file system `event` (CREATE|WRITE|REMOVE|RENAME|CHMOD)")
48+
polling = pflag.Duration("polling", 0, "poll files at given `interval` instead of using fsnotify")
4749
)
4850

4951
func main() {
@@ -89,7 +91,7 @@ func main() {
8991
os.Exit(1)
9092
}
9193

92-
modC, errC, err := watcher(*targets, *patterns, *ignores, filtOp)
94+
modC, errC, err := watcher(*targets, *patterns, *ignores, filtOp, *polling)
9395
if err != nil {
9496
log.Fatalf("[ARELO] wacher error: %v", err)
9597
}
@@ -166,12 +168,18 @@ func parseFilters(filters []string) (fsnotify.Op, error) {
166168
return op, nil
167169
}
168170

169-
func watcher(targets, patterns, ignores []string, filtOp fsnotify.Op) (<-chan string, <-chan error, error) {
170-
w, err := fsnotify.NewWatcher()
171+
func newWatcher(interval time.Duration) (fspoll.Watcher, error) {
172+
if interval == 0 {
173+
return fspoll.Wrap(fsnotify.NewWatcher())
174+
}
175+
return fspoll.New(interval), nil
176+
}
177+
178+
func watcher(targets, patterns, ignores []string, filtOp fsnotify.Op, interval time.Duration) (<-chan string, <-chan error, error) {
179+
w, err := newWatcher(interval)
171180
if err != nil {
172181
return nil, nil, err
173182
}
174-
175183
if err := addTargets(w, targets, patterns, ignores); err != nil {
176184
return nil, nil, err
177185
}
@@ -184,7 +192,7 @@ func watcher(targets, patterns, ignores []string, filtOp fsnotify.Op) (<-chan st
184192
defer close(modC)
185193
for {
186194
select {
187-
case event, ok := <-w.Events:
195+
case event, ok := <-w.Events():
188196
if !ok {
189197
errC <- xerrors.Errorf("watcher.Events closed")
190198
return
@@ -216,15 +224,15 @@ func watcher(targets, patterns, ignores []string, filtOp fsnotify.Op) (<-chan st
216224
// ignore stat errors (notfound, permission, etc.)
217225
log.Printf("[ARELO] watcher: %v", err)
218226
} else if fi.IsDir() {
219-
err := addDirRecursive(w, fi, name, patterns, ignores, modC)
227+
err := addDirRecursive(w, name, patterns, ignores, modC)
220228
if err != nil {
221229
errC <- err
222230
return
223231
}
224232
}
225233
}
226234

227-
case err, ok := <-w.Errors:
235+
case err, ok := <-w.Errors():
228236
errC <- xerrors.Errorf("watcher.Errors (%v): %w", ok, err)
229237
return
230238
}
@@ -256,15 +264,15 @@ func matchPatterns(t string, pats []string) (bool, error) {
256264
return false, nil
257265
}
258266

259-
func addTargets(w *fsnotify.Watcher, targets, patterns, ignores []string) error {
267+
func addTargets(w fspoll.Watcher, targets, patterns, ignores []string) error {
260268
for _, t := range targets {
261269
t = path.Clean(t)
262270
fi, err := os.Stat(t)
263271
if err != nil {
264272
return xerrors.Errorf("stat: %w", err)
265273
}
266274
if fi.IsDir() {
267-
if err := addDirRecursive(w, fi, t, patterns, ignores, nil); err != nil {
275+
if err := addDirRecursive(w, t, patterns, ignores, nil); err != nil {
268276
return err
269277
}
270278
}
@@ -276,7 +284,7 @@ func addTargets(w *fsnotify.Watcher, targets, patterns, ignores []string) error
276284
return nil
277285
}
278286

279-
func addDirRecursive(w *fsnotify.Watcher, fi fs.FileInfo, t string, patterns, ignores []string, ch chan<- string) error {
287+
func addDirRecursive(w fspoll.Watcher, t string, patterns, ignores []string, ch chan<- string) error {
280288
logVerbose("watching target: %q", t)
281289
err := w.Add(t)
282290
if err != nil {
@@ -301,11 +309,7 @@ func addDirRecursive(w *fsnotify.Watcher, fi fs.FileInfo, t string, patterns, ig
301309
}
302310
}
303311
if de.IsDir() {
304-
fi, err := de.Info()
305-
if err != nil {
306-
return err
307-
}
308-
err = addDirRecursive(w, fi, name, patterns, ignores, ch)
312+
err = addDirRecursive(w, name, patterns, ignores, ch)
309313
if err != nil {
310314
return err
311315
}

arelo_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ import (
77
"time"
88
)
99

10-
func TestWatcher(t *testing.T) {
10+
func TestWatcherFsnotify(t *testing.T) {
11+
t.Parallel()
12+
testWatcher(t, 0)
13+
}
14+
15+
func TestWatcherFspoll(t *testing.T) {
16+
t.Parallel()
17+
testWatcher(t, time.Second/10)
18+
}
19+
20+
func testWatcher(t *testing.T, polling time.Duration) {
1121
tmpdir := t.TempDir()
1222

1323
dirs := []string{
@@ -26,7 +36,7 @@ func TestWatcher(t *testing.T) {
2636
ignores := []string{"**/ignore"}
2737
patterns := []string{"**/file"}
2838

29-
modC, errC, err := watcher(targets, patterns, ignores, 0)
39+
modC, errC, err := watcher(targets, patterns, ignores, 0, polling)
3040
if err != nil {
3141
t.Fatalf("watcher: %v", err)
3242
}
@@ -51,6 +61,7 @@ func TestWatcher(t *testing.T) {
5161
for _, test := range tests {
5262
<-time.After(time.Second / 5)
5363
clearChan(modC, errC)
64+
t.Logf("touch %v => detect %v", test.file, test.detect)
5465
touchFile(test.file)
5566
select {
5667
case f := <-modC:

fspoll/fsnotify.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fspoll
2+
3+
import (
4+
"github.com/fsnotify/fsnotify"
5+
)
6+
7+
// Wrapper of a fsnotify.Watcher.
8+
type Wrapper struct {
9+
*fsnotify.Watcher
10+
}
11+
12+
var _ Watcher = Wrapper{}
13+
14+
// Wrap returns a wrapping fsnotify.Watcher.
15+
func Wrap(w *fsnotify.Watcher, err error) (Wrapper, error) {
16+
return Wrapper{w}, err
17+
}
18+
19+
// Events returns Events channel of wrapping fsnotify.Watcher.
20+
func (w Wrapper) Events() <-chan Event {
21+
return w.Watcher.Events
22+
}
23+
24+
// Errors returns Errors channel of wrapping fsnotify.Watcher.
25+
func (w Wrapper) Errors() <-chan error {
26+
return w.Watcher.Errors
27+
}

fspoll/fspoll.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// fspoll provides polling file change watcher.
2+
package fspoll
3+
4+
import (
5+
"github.com/fsnotify/fsnotify"
6+
)
7+
8+
type Event = fsnotify.Event
9+
type Op = fsnotify.Op
10+
11+
const (
12+
Create = fsnotify.Create
13+
Write = fsnotify.Write
14+
Remove = fsnotify.Remove
15+
Rename = fsnotify.Rename
16+
Chmod = fsnotify.Chmod
17+
)
18+
19+
var (
20+
ErrNonExistentWatch = fsnotify.ErrNonExistentWatch
21+
ErrEventOverflow = fsnotify.ErrEventOverflow
22+
ErrClosed = fsnotify.ErrClosed
23+
)
24+
25+
// Watcher is a common interface for fspoll and fsnotify
26+
type Watcher interface {
27+
28+
// Add starts watching the path for changes.
29+
Add(name string) error
30+
31+
// Close stops all watches and closes the channels.
32+
Close() error
33+
34+
// Remove stops watching the specified path.
35+
Remove(name string) error
36+
37+
// WatchList returns a list of watching path names.
38+
WatchList() []string
39+
40+
// Events returns a channel that receives filesystem events.
41+
Events() <-chan Event
42+
43+
// Errors returns a channel that receives errors.
44+
Errors() <-chan error
45+
}

0 commit comments

Comments
 (0)