-
Notifications
You must be signed in to change notification settings - Fork 2
/
watcher.go
169 lines (142 loc) · 4.26 KB
/
watcher.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// Package watcher is used to keep an eye on file system events,
// and trigger actions when those events are of interest.
package watcher
import (
"github.com/fsnotify/fsnotify"
"io"
"log"
"strings"
"sync"
"golang.org/x/sync/singleflight"
)
var (
// DebugOut is a log.Logger for debug messages
DebugOut = log.New(io.Discard, "[DEBUG] ", 0)
// ErrorOut is a log.Logger for error messages
ErrorOut = log.New(io.Discard, "", 0)
)
// WatchHandlerFunc takes an fsnotify.Event to add on. A WatchHandlerFunc can assume it will not be called more than once while running
type WatchHandlerFunc func(fsnotify.Event)
// Watcher is an implementation of fsnotify.Watcher with some rails
// and opinions.
type Watcher struct {
// Close should be called when the Watcher is no longer of use. It is safe to call multiple times.
Close func()
fs *fsnotify.Watcher
watchMap sync.Map
errorChan chan error
}
// NewWatcher returns a Watcher or an error if a problem occurred creating it (very rare)
func NewWatcher() (*Watcher, error) {
return NewWatcherWithErrorChan(make(chan error))
}
// NewWatcherWithErrorChan returns a Watcher or an error if a problem occurred creating it (very rare). The supplied chan will receive non-nil
// errors if they occur
func NewWatcherWithErrorChan(errorChan chan error) (*Watcher, error) {
var (
watcher Watcher
err error
)
watcher.errorChan = errorChan
watcher.fs, err = fsnotify.NewWatcher()
if err != nil {
ErrorOut.Println(err)
return nil, err
}
done := make(chan struct{})
watcher.Close = func() {
watcher.Close = func() {}
close(done)
}
watcher.run(done)
return &watcher, nil
}
// Add takes a filename, and a WatchHandlerFunc to call if the file changes
func (w *Watcher) Add(file string, wf WatchHandlerFunc) error {
file = strings.TrimPrefix(file, "file://")
err := w.fs.Add(file)
if err != nil {
return err
}
w.watchMap.Store(file, wf)
return nil
}
// Delete takes a filename, and removes it from being watched
func (w *Watcher) Delete(file string) error {
file = strings.TrimPrefix(file, "file://")
err := w.fs.Remove(file)
if err != nil {
return err
}
w.watchMap.Delete(file)
return nil
}
// run fires off a goto to run the main watch loop
func (w *Watcher) run(done chan struct{}) {
go func() {
defer w.fs.Close()
defer close(w.errorChan)
// Using singleflight to ensure we're not spamming WatchHandlerFuncs
var requestGroup singleflight.Group
for {
select {
case event, cok := <-w.fs.Events:
DebugOut.Printf("Watched event: %+v\n", event)
if !cok {
ErrorOut.Printf("Fsnotify channel apparently closed!\n")
return
}
if event.Name == "" {
continue
}
var (
wfunc WatchHandlerFunc
)
if wfunci, ok := w.watchMap.Load(event.Name); !ok {
ErrorOut.Printf("Events for '%s' but no map entry?!\n", event.Name)
continue
} else {
// Assert it
wfunc = wfunci.(WatchHandlerFunc)
}
switch event.Op {
case fsnotify.Write:
go requestGroup.Do(event.Name, func() (interface{}, error) {
wfunc(event)
return nil, nil
})
/*
case fsnotify.Remove, fsnotify.Rename:
// TODO: TL;DR: This is largely broken. Thankfully, edge case.
// It appears to be a bug in fsnotify, and I haven't waded through it. The 99% case is "Write", which is handled above and
// works fine. If you use an editor to make changes that moves and swaps, then you get here, and the file ends up orphaned, even
// though it shouldn't, and no errors occur, just that the next event that fires has no name, so it can't be handled.
if waiting {
continue
}
waiting = true // skip subsequent notifies until we handle one
go HandleReload(&waiting, event.Name, mfiles)
// We have to re-add the file. Lame, I know
err := watcher.Add(event.Name)
if err != nil {
DebugOut.Printf("Error re-adding '%s' to the watcher: %s\n", event.Name, err)
}
*/
}
case err, ok := <-w.fs.Errors:
if !ok {
ErrorOut.Printf("Fsnotify channel apparently closed!\n")
return
}
ErrorOut.Println("Fsnotify error:", err)
// Don't block or freak out
select {
case w.errorChan <- err:
default:
}
case <-done:
return
}
}
}()
}