Browse code

Fallback to file polling for jsonlog reader on err

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2015/10/09 05:55:28
Showing 6 changed files
... ...
@@ -7,29 +7,20 @@ import (
7 7
 	"bytes"
8 8
 	"encoding/json"
9 9
 	"fmt"
10
-	"io"
11 10
 	"os"
12 11
 	"strconv"
13 12
 	"sync"
14
-	"time"
15
-
16
-	"gopkg.in/fsnotify.v1"
17 13
 
18 14
 	"github.com/Sirupsen/logrus"
19 15
 	"github.com/docker/docker/daemon/logger"
20
-	"github.com/docker/docker/pkg/ioutils"
21 16
 	"github.com/docker/docker/pkg/jsonlog"
22 17
 	"github.com/docker/docker/pkg/pubsub"
23
-	"github.com/docker/docker/pkg/tailfile"
24 18
 	"github.com/docker/docker/pkg/timeutils"
25 19
 	"github.com/docker/docker/pkg/units"
26 20
 )
27 21
 
28
-const (
29
-	// Name is the name of the file that the jsonlogger logs to.
30
-	Name               = "json-file"
31
-	maxJSONDecodeRetry = 20000
32
-)
22
+// Name is the name of the file that the jsonlogger logs to.
23
+const Name = "json-file"
33 24
 
34 25
 // JSONFileLogger is Logger implementation for default Docker logging.
35 26
 type JSONFileLogger struct {
... ...
@@ -228,193 +219,3 @@ func (l *JSONFileLogger) Close() error {
228 228
 func (l *JSONFileLogger) Name() string {
229 229
 	return Name
230 230
 }
231
-
232
-func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) {
233
-	l.Reset()
234
-	if err := dec.Decode(l); err != nil {
235
-		return nil, err
236
-	}
237
-	msg := &logger.Message{
238
-		Source:    l.Stream,
239
-		Timestamp: l.Created,
240
-		Line:      []byte(l.Log),
241
-	}
242
-	return msg, nil
243
-}
244
-
245
-// ReadLogs implements the logger's LogReader interface for the logs
246
-// created by this driver.
247
-func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
248
-	logWatcher := logger.NewLogWatcher()
249
-
250
-	go l.readLogs(logWatcher, config)
251
-	return logWatcher
252
-}
253
-
254
-func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
255
-	defer close(logWatcher.Msg)
256
-
257
-	pth := l.ctx.LogPath
258
-	var files []io.ReadSeeker
259
-	for i := l.n; i > 1; i-- {
260
-		f, err := os.Open(fmt.Sprintf("%s.%d", pth, i-1))
261
-		if err != nil {
262
-			if !os.IsNotExist(err) {
263
-				logWatcher.Err <- err
264
-				break
265
-			}
266
-			continue
267
-		}
268
-		defer f.Close()
269
-		files = append(files, f)
270
-	}
271
-
272
-	latestFile, err := os.Open(pth)
273
-	if err != nil {
274
-		logWatcher.Err <- err
275
-		return
276
-	}
277
-	defer latestFile.Close()
278
-
279
-	files = append(files, latestFile)
280
-	tailer := ioutils.MultiReadSeeker(files...)
281
-
282
-	if config.Tail != 0 {
283
-		tailFile(tailer, logWatcher, config.Tail, config.Since)
284
-	}
285
-
286
-	if !config.Follow {
287
-		return
288
-	}
289
-
290
-	if config.Tail >= 0 {
291
-		latestFile.Seek(0, os.SEEK_END)
292
-	}
293
-
294
-	l.mu.Lock()
295
-	l.readers[logWatcher] = struct{}{}
296
-	l.mu.Unlock()
297
-
298
-	notifyRotate := l.notifyRotate.Subscribe()
299
-	followLogs(latestFile, logWatcher, notifyRotate, config.Since)
300
-
301
-	l.mu.Lock()
302
-	delete(l.readers, logWatcher)
303
-	l.mu.Unlock()
304
-
305
-	l.notifyRotate.Evict(notifyRotate)
306
-}
307
-
308
-func tailFile(f io.ReadSeeker, logWatcher *logger.LogWatcher, tail int, since time.Time) {
309
-	var rdr io.Reader = f
310
-	if tail > 0 {
311
-		ls, err := tailfile.TailFile(f, tail)
312
-		if err != nil {
313
-			logWatcher.Err <- err
314
-			return
315
-		}
316
-		rdr = bytes.NewBuffer(bytes.Join(ls, []byte("\n")))
317
-	}
318
-	dec := json.NewDecoder(rdr)
319
-	l := &jsonlog.JSONLog{}
320
-	for {
321
-		msg, err := decodeLogLine(dec, l)
322
-		if err != nil {
323
-			if err != io.EOF {
324
-				logWatcher.Err <- err
325
-			}
326
-			return
327
-		}
328
-		if !since.IsZero() && msg.Timestamp.Before(since) {
329
-			continue
330
-		}
331
-		logWatcher.Msg <- msg
332
-	}
333
-}
334
-
335
-func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, since time.Time) {
336
-	dec := json.NewDecoder(f)
337
-	l := &jsonlog.JSONLog{}
338
-	fileWatcher, err := fsnotify.NewWatcher()
339
-	if err != nil {
340
-		logWatcher.Err <- err
341
-		return
342
-	}
343
-	defer fileWatcher.Close()
344
-	if err := fileWatcher.Add(f.Name()); err != nil {
345
-		logWatcher.Err <- err
346
-		return
347
-	}
348
-
349
-	var retries int
350
-	for {
351
-		msg, err := decodeLogLine(dec, l)
352
-		if err != nil {
353
-			if err != io.EOF {
354
-				// try again because this shouldn't happen
355
-				if _, ok := err.(*json.SyntaxError); ok && retries <= maxJSONDecodeRetry {
356
-					dec = json.NewDecoder(f)
357
-					retries++
358
-					continue
359
-				}
360
-
361
-				// io.ErrUnexpectedEOF is returned from json.Decoder when there is
362
-				// remaining data in the parser's buffer while an io.EOF occurs.
363
-				// If the json logger writes a partial json log entry to the disk
364
-				// while at the same time the decoder tries to decode it, the race codition happens.
365
-				if err == io.ErrUnexpectedEOF && retries <= maxJSONDecodeRetry {
366
-					reader := io.MultiReader(dec.Buffered(), f)
367
-					dec = json.NewDecoder(reader)
368
-					retries++
369
-					continue
370
-				}
371
-				logWatcher.Err <- err
372
-				return
373
-			}
374
-
375
-			select {
376
-			case <-fileWatcher.Events:
377
-				dec = json.NewDecoder(f)
378
-				continue
379
-			case <-fileWatcher.Errors:
380
-				logWatcher.Err <- err
381
-				return
382
-			case <-logWatcher.WatchClose():
383
-				return
384
-			case <-notifyRotate:
385
-				fileWatcher.Remove(f.Name())
386
-
387
-				f, err = os.Open(f.Name())
388
-				if err != nil {
389
-					logWatcher.Err <- err
390
-					return
391
-				}
392
-				if err := fileWatcher.Add(f.Name()); err != nil {
393
-					logWatcher.Err <- err
394
-				}
395
-				dec = json.NewDecoder(f)
396
-				continue
397
-			}
398
-		}
399
-
400
-		retries = 0 // reset retries since we've succeeded
401
-		if !since.IsZero() && msg.Timestamp.Before(since) {
402
-			continue
403
-		}
404
-		select {
405
-		case logWatcher.Msg <- msg:
406
-		case <-logWatcher.WatchClose():
407
-			logWatcher.Msg <- msg
408
-			for {
409
-				msg, err := decodeLogLine(dec, l)
410
-				if err != nil {
411
-					return
412
-				}
413
-				if !since.IsZero() && msg.Timestamp.Before(since) {
414
-					continue
415
-				}
416
-				logWatcher.Msg <- msg
417
-			}
418
-		}
419
-	}
420
-}
421 231
new file mode 100644
... ...
@@ -0,0 +1,216 @@
0
+package jsonfilelog
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"os"
8
+	"time"
9
+
10
+	"github.com/Sirupsen/logrus"
11
+	"github.com/docker/docker/daemon/logger"
12
+	"github.com/docker/docker/pkg/filenotify"
13
+	"github.com/docker/docker/pkg/ioutils"
14
+	"github.com/docker/docker/pkg/jsonlog"
15
+	"github.com/docker/docker/pkg/tailfile"
16
+)
17
+
18
+const maxJSONDecodeRetry = 20000
19
+
20
+func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) {
21
+	l.Reset()
22
+	if err := dec.Decode(l); err != nil {
23
+		return nil, err
24
+	}
25
+	msg := &logger.Message{
26
+		Source:    l.Stream,
27
+		Timestamp: l.Created,
28
+		Line:      []byte(l.Log),
29
+	}
30
+	return msg, nil
31
+}
32
+
33
+// ReadLogs implements the logger's LogReader interface for the logs
34
+// created by this driver.
35
+func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
36
+	logWatcher := logger.NewLogWatcher()
37
+
38
+	go l.readLogs(logWatcher, config)
39
+	return logWatcher
40
+}
41
+
42
+func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
43
+	defer close(logWatcher.Msg)
44
+
45
+	pth := l.ctx.LogPath
46
+	var files []io.ReadSeeker
47
+	for i := l.n; i > 1; i-- {
48
+		f, err := os.Open(fmt.Sprintf("%s.%d", pth, i-1))
49
+		if err != nil {
50
+			if !os.IsNotExist(err) {
51
+				logWatcher.Err <- err
52
+				break
53
+			}
54
+			continue
55
+		}
56
+		defer f.Close()
57
+		files = append(files, f)
58
+	}
59
+
60
+	latestFile, err := os.Open(pth)
61
+	if err != nil {
62
+		logWatcher.Err <- err
63
+		return
64
+	}
65
+	defer latestFile.Close()
66
+
67
+	files = append(files, latestFile)
68
+	tailer := ioutils.MultiReadSeeker(files...)
69
+
70
+	if config.Tail != 0 {
71
+		tailFile(tailer, logWatcher, config.Tail, config.Since)
72
+	}
73
+
74
+	if !config.Follow {
75
+		return
76
+	}
77
+
78
+	if config.Tail >= 0 {
79
+		latestFile.Seek(0, os.SEEK_END)
80
+	}
81
+
82
+	l.mu.Lock()
83
+	l.readers[logWatcher] = struct{}{}
84
+	l.mu.Unlock()
85
+
86
+	notifyRotate := l.notifyRotate.Subscribe()
87
+	followLogs(latestFile, logWatcher, notifyRotate, config.Since)
88
+
89
+	l.mu.Lock()
90
+	delete(l.readers, logWatcher)
91
+	l.mu.Unlock()
92
+
93
+	l.notifyRotate.Evict(notifyRotate)
94
+}
95
+
96
+func tailFile(f io.ReadSeeker, logWatcher *logger.LogWatcher, tail int, since time.Time) {
97
+	var rdr io.Reader = f
98
+	if tail > 0 {
99
+		ls, err := tailfile.TailFile(f, tail)
100
+		if err != nil {
101
+			logWatcher.Err <- err
102
+			return
103
+		}
104
+		rdr = bytes.NewBuffer(bytes.Join(ls, []byte("\n")))
105
+	}
106
+	dec := json.NewDecoder(rdr)
107
+	l := &jsonlog.JSONLog{}
108
+	for {
109
+		msg, err := decodeLogLine(dec, l)
110
+		if err != nil {
111
+			if err != io.EOF {
112
+				logWatcher.Err <- err
113
+			}
114
+			return
115
+		}
116
+		if !since.IsZero() && msg.Timestamp.Before(since) {
117
+			continue
118
+		}
119
+		logWatcher.Msg <- msg
120
+	}
121
+}
122
+
123
+func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, since time.Time) {
124
+	dec := json.NewDecoder(f)
125
+	l := &jsonlog.JSONLog{}
126
+
127
+	fileWatcher, err := filenotify.New()
128
+	if err != nil {
129
+		logWatcher.Err <- err
130
+	}
131
+	defer fileWatcher.Close()
132
+
133
+	var retries int
134
+	for {
135
+		msg, err := decodeLogLine(dec, l)
136
+		if err != nil {
137
+			if err != io.EOF {
138
+				// try again because this shouldn't happen
139
+				if _, ok := err.(*json.SyntaxError); ok && retries <= maxJSONDecodeRetry {
140
+					dec = json.NewDecoder(f)
141
+					retries++
142
+					continue
143
+				}
144
+
145
+				// io.ErrUnexpectedEOF is returned from json.Decoder when there is
146
+				// remaining data in the parser's buffer while an io.EOF occurs.
147
+				// If the json logger writes a partial json log entry to the disk
148
+				// while at the same time the decoder tries to decode it, the race codition happens.
149
+				if err == io.ErrUnexpectedEOF && retries <= maxJSONDecodeRetry {
150
+					reader := io.MultiReader(dec.Buffered(), f)
151
+					dec = json.NewDecoder(reader)
152
+					retries++
153
+					continue
154
+				}
155
+				logWatcher.Err <- err
156
+				return
157
+			}
158
+
159
+			logrus.WithField("logger", "json-file").Debugf("waiting for events")
160
+			if err := fileWatcher.Add(f.Name()); err != nil {
161
+				logrus.WithField("logger", "json-file").Warn("falling back to file poller")
162
+				fileWatcher.Close()
163
+				fileWatcher = filenotify.NewPollingWatcher()
164
+				if err := fileWatcher.Add(f.Name()); err != nil {
165
+					logrus.Errorf("error watching log file for modifications: %v", err)
166
+					logWatcher.Err <- err
167
+				}
168
+			}
169
+			select {
170
+			case <-fileWatcher.Events():
171
+				dec = json.NewDecoder(f)
172
+				fileWatcher.Remove(f.Name())
173
+				continue
174
+			case <-fileWatcher.Errors():
175
+				fileWatcher.Remove(f.Name())
176
+				logWatcher.Err <- err
177
+				return
178
+			case <-logWatcher.WatchClose():
179
+				fileWatcher.Remove(f.Name())
180
+				return
181
+			case <-notifyRotate:
182
+				f, err = os.Open(f.Name())
183
+				if err != nil {
184
+					logWatcher.Err <- err
185
+					return
186
+				}
187
+
188
+				dec = json.NewDecoder(f)
189
+				fileWatcher.Remove(f.Name())
190
+				fileWatcher.Add(f.Name())
191
+				continue
192
+			}
193
+		}
194
+
195
+		retries = 0 // reset retries since we've succeeded
196
+		if !since.IsZero() && msg.Timestamp.Before(since) {
197
+			continue
198
+		}
199
+		select {
200
+		case logWatcher.Msg <- msg:
201
+		case <-logWatcher.WatchClose():
202
+			logWatcher.Msg <- msg
203
+			for {
204
+				msg, err := decodeLogLine(dec, l)
205
+				if err != nil {
206
+					return
207
+				}
208
+				if !since.IsZero() && msg.Timestamp.Before(since) {
209
+					continue
210
+				}
211
+				logWatcher.Msg <- msg
212
+			}
213
+		}
214
+	}
215
+}
0 216
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+// Package filenotify provides a mechanism for watching file(s) for changes.
1
+// Generally leans on fsnotify, but provides a poll-based notifier which fsnotify does not support.
2
+// These are wrapped up in a common interface so that either can be used interchangably in your code.
3
+package filenotify
4
+
5
+import "gopkg.in/fsnotify.v1"
6
+
7
+// FileWatcher is an interface for implementing file notification watchers
8
+type FileWatcher interface {
9
+	Events() <-chan fsnotify.Event
10
+	Errors() <-chan error
11
+	Add(name string) error
12
+	Remove(name string) error
13
+	Close() error
14
+}
15
+
16
+// New tries to use an fs-event watcher, and falls back to the poller if there is an error
17
+func New() (FileWatcher, error) {
18
+	if watcher, err := NewEventWatcher(); err == nil {
19
+		return watcher, nil
20
+	}
21
+	return NewPollingWatcher(), nil
22
+}
23
+
24
+// NewPollingWatcher returns a poll-based file watcher
25
+func NewPollingWatcher() FileWatcher {
26
+	return &filePoller{
27
+		events: make(chan fsnotify.Event),
28
+		errors: make(chan error),
29
+	}
30
+}
31
+
32
+// NewEventWatcher returns an fs-event based file watcher
33
+func NewEventWatcher() (FileWatcher, error) {
34
+	watcher, err := fsnotify.NewWatcher()
35
+	if err != nil {
36
+		return nil, err
37
+	}
38
+	return &fsNotifyWatcher{watcher}, nil
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package filenotify
1
+
2
+import "gopkg.in/fsnotify.v1"
3
+
4
+// fsNotify wraps the fsnotify package to satisfy the FileNotifer interface
5
+type fsNotifyWatcher struct {
6
+	*fsnotify.Watcher
7
+}
8
+
9
+// GetEvents returns the fsnotify event channel receiver
10
+func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event {
11
+	return w.Watcher.Events
12
+}
13
+
14
+// GetErrors returns the fsnotify error channel receiver
15
+func (w *fsNotifyWatcher) Errors() <-chan error {
16
+	return w.Watcher.Errors
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,205 @@
0
+package filenotify
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"os"
6
+	"sync"
7
+	"time"
8
+
9
+	"github.com/Sirupsen/logrus"
10
+
11
+	"gopkg.in/fsnotify.v1"
12
+)
13
+
14
+var (
15
+	// errPollerClosed is returned when the poller is closed
16
+	errPollerClosed = errors.New("poller is closed")
17
+	// errNoSuchPoller is returned when trying to remove a watch that doesn't exist
18
+	errNoSuchWatch = errors.New("poller does not exist")
19
+)
20
+
21
+// watchWaitTime is the time to wait between file poll loops
22
+const watchWaitTime = 200 * time.Millisecond
23
+
24
+// filePoller is used to poll files for changes, especially in cases where fsnotify
25
+// can't be run (e.g. when inotify handles are exhausted)
26
+// filePoller satifies the FileWatcher interface
27
+type filePoller struct {
28
+	// watches is the list of files currently being polled, close the associated channel to stop the watch
29
+	watches map[string]chan struct{}
30
+	// events is the channel to listen to for watch events
31
+	events chan fsnotify.Event
32
+	// errors is the channel to listen to for watch errors
33
+	errors chan error
34
+	// mu locks the poller for modification
35
+	mu sync.Mutex
36
+	// closed is used to specify when the poller has already closed
37
+	closed bool
38
+}
39
+
40
+// Add adds a filename to the list of watches
41
+// once added the file is polled for changes in a separate goroutine
42
+func (w *filePoller) Add(name string) error {
43
+	w.mu.Lock()
44
+	defer w.mu.Unlock()
45
+
46
+	if w.closed == true {
47
+		return errPollerClosed
48
+	}
49
+
50
+	f, err := os.Open(name)
51
+	if err != nil {
52
+		return err
53
+	}
54
+	fi, err := os.Stat(name)
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	if w.watches == nil {
60
+		w.watches = make(map[string]chan struct{})
61
+	}
62
+	if _, exists := w.watches[name]; exists {
63
+		return fmt.Errorf("watch exists")
64
+	}
65
+	chClose := make(chan struct{})
66
+	w.watches[name] = chClose
67
+
68
+	go w.watch(f, fi, chClose)
69
+	return nil
70
+}
71
+
72
+// Remove stops and removes watch with the specified name
73
+func (w *filePoller) Remove(name string) error {
74
+	w.mu.Lock()
75
+	defer w.mu.Unlock()
76
+	return w.remove(name)
77
+}
78
+
79
+func (w *filePoller) remove(name string) error {
80
+	if w.closed == true {
81
+		return errPollerClosed
82
+	}
83
+
84
+	chClose, exists := w.watches[name]
85
+	if !exists {
86
+		return errNoSuchWatch
87
+	}
88
+	close(chClose)
89
+	delete(w.watches, name)
90
+	return nil
91
+}
92
+
93
+// Events returns the event channel
94
+// This is used for notifications on events about watched files
95
+func (w *filePoller) Events() <-chan fsnotify.Event {
96
+	return w.events
97
+}
98
+
99
+// Errors returns the errors channel
100
+// This is used for notifications about errors on watched files
101
+func (w *filePoller) Errors() <-chan error {
102
+	return w.errors
103
+}
104
+
105
+// Close closes the poller
106
+// All watches are stopped, removed, and the poller cannot be added to
107
+func (w *filePoller) Close() error {
108
+	w.mu.Lock()
109
+	defer w.mu.Unlock()
110
+
111
+	if w.closed {
112
+		return nil
113
+	}
114
+
115
+	w.closed = true
116
+	for name := range w.watches {
117
+		w.remove(name)
118
+		delete(w.watches, name)
119
+	}
120
+	close(w.events)
121
+	close(w.errors)
122
+	return nil
123
+}
124
+
125
+// sendEvent publishes the specified event to the events channel
126
+func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error {
127
+	select {
128
+	case w.events <- e:
129
+	case <-chClose:
130
+		return fmt.Errorf("closed")
131
+	}
132
+	return nil
133
+}
134
+
135
+// sendErr publishes the specified error to the errors channel
136
+func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error {
137
+	select {
138
+	case w.errors <- e:
139
+	case <-chClose:
140
+		return fmt.Errorf("closed")
141
+	}
142
+	return nil
143
+}
144
+
145
+// watch is responsible for polling the specified file for changes
146
+// upon finding changes to a file or errors, sendEvent/sendErr is called
147
+func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{}) {
148
+	for {
149
+		time.Sleep(watchWaitTime)
150
+		select {
151
+		case <-chClose:
152
+			logrus.Debugf("watch for %s closed", f.Name())
153
+			return
154
+		default:
155
+		}
156
+
157
+		fi, err := os.Stat(f.Name())
158
+		if err != nil {
159
+			// if we got an error here and lastFi is not set, we can presume that nothing has changed
160
+			// This should be safe since before `watch()` is called, a stat is performed, there is any error `watch` is not called
161
+			if lastFi == nil {
162
+				continue
163
+			}
164
+			// If it doesn't exist at this point, it must have been removed
165
+			// no need to send the error here since this is a valid operation
166
+			if os.IsNotExist(err) {
167
+				if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Remove, Name: f.Name()}, chClose); err != nil {
168
+					return
169
+				}
170
+				lastFi = nil
171
+				continue
172
+			}
173
+			// at this point, send the error
174
+			if err := w.sendErr(err, chClose); err != nil {
175
+				return
176
+			}
177
+			continue
178
+		}
179
+
180
+		if lastFi == nil {
181
+			if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: fi.Name()}, chClose); err != nil {
182
+				return
183
+			}
184
+			lastFi = fi
185
+			continue
186
+		}
187
+
188
+		if fi.Mode() != lastFi.Mode() {
189
+			if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: fi.Name()}, chClose); err != nil {
190
+				return
191
+			}
192
+			lastFi = fi
193
+			continue
194
+		}
195
+
196
+		if fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size() {
197
+			if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: fi.Name()}, chClose); err != nil {
198
+				return
199
+			}
200
+			lastFi = fi
201
+			continue
202
+		}
203
+	}
204
+}
0 205
new file mode 100644
... ...
@@ -0,0 +1,133 @@
0
+package filenotify
1
+
2
+import (
3
+	"fmt"
4
+	"io/ioutil"
5
+	"os"
6
+	"testing"
7
+	"time"
8
+
9
+	"gopkg.in/fsnotify.v1"
10
+)
11
+
12
+func TestPollerAddRemove(t *testing.T) {
13
+	w := NewPollingWatcher()
14
+
15
+	if err := w.Add("no-such-file"); err == nil {
16
+		t.Fatal("should have gotten error when adding a non-existant file")
17
+	}
18
+	if err := w.Remove("no-such-file"); err == nil {
19
+		t.Fatal("should have gotten error when removing non-existant watch")
20
+	}
21
+
22
+	f, err := ioutil.TempFile("", "asdf")
23
+	if err != nil {
24
+		t.Fatal(err)
25
+	}
26
+	defer os.RemoveAll(f.Name())
27
+
28
+	if err := w.Add(f.Name()); err != nil {
29
+		t.Fatal(err)
30
+	}
31
+
32
+	if err := w.Remove(f.Name()); err != nil {
33
+		t.Fatal(err)
34
+	}
35
+}
36
+
37
+func TestPollerEvent(t *testing.T) {
38
+	w := NewPollingWatcher()
39
+
40
+	f, err := ioutil.TempFile("", "test-poller")
41
+	if err != nil {
42
+		t.Fatal("error creating temp file")
43
+	}
44
+	defer os.RemoveAll(f.Name())
45
+	f.Close()
46
+
47
+	if err := w.Add(f.Name()); err != nil {
48
+		t.Fatal(err)
49
+	}
50
+
51
+	select {
52
+	case <-w.Events():
53
+		t.Fatal("got event before anything happened")
54
+	case <-w.Errors():
55
+		t.Fatal("got error before anything happened")
56
+	default:
57
+	}
58
+
59
+	if err := ioutil.WriteFile(f.Name(), []byte("hello"), 644); err != nil {
60
+		t.Fatal(err)
61
+	}
62
+	if err := assertEvent(w, fsnotify.Write); err != nil {
63
+		t.Fatal(err)
64
+	}
65
+
66
+	if err := os.Chmod(f.Name(), 600); err != nil {
67
+		t.Fatal(err)
68
+	}
69
+	if err := assertEvent(w, fsnotify.Chmod); err != nil {
70
+		t.Fatal(err)
71
+	}
72
+
73
+	if err := os.Remove(f.Name()); err != nil {
74
+		t.Fatal(err)
75
+	}
76
+	if err := assertEvent(w, fsnotify.Remove); err != nil {
77
+		t.Fatal(err)
78
+	}
79
+}
80
+
81
+func TestPollerClose(t *testing.T) {
82
+	w := NewPollingWatcher()
83
+	if err := w.Close(); err != nil {
84
+		t.Fatal(err)
85
+	}
86
+	// test double-close
87
+	if err := w.Close(); err != nil {
88
+		t.Fatal(err)
89
+	}
90
+
91
+	select {
92
+	case _, open := <-w.Events():
93
+		if open {
94
+			t.Fatal("event chan should be closed")
95
+		}
96
+	default:
97
+		t.Fatal("event chan should be closed")
98
+	}
99
+
100
+	select {
101
+	case _, open := <-w.Errors():
102
+		if open {
103
+			t.Fatal("errors chan should be closed")
104
+		}
105
+	default:
106
+		t.Fatal("errors chan should be closed")
107
+	}
108
+
109
+	f, err := ioutil.TempFile("", "asdf")
110
+	if err != nil {
111
+		t.Fatal(err)
112
+	}
113
+	defer os.RemoveAll(f.Name())
114
+	if err := w.Add(f.Name()); err == nil {
115
+		t.Fatal("should have gotten error adding watch for closed watcher")
116
+	}
117
+}
118
+
119
+func assertEvent(w FileWatcher, eType fsnotify.Op) error {
120
+	var err error
121
+	select {
122
+	case e := <-w.Events():
123
+		if e.Op != eType {
124
+			err = fmt.Errorf("got wrong event type, expected %q: %v", eType, e)
125
+		}
126
+	case e := <-w.Errors():
127
+		err = fmt.Errorf("got unexpected error waiting for events %v: %v", eType, e)
128
+	case <-time.After(watchWaitTime * 3):
129
+		err = fmt.Errorf("timeout waiting for event %v", eType)
130
+	}
131
+	return err
132
+}