Signed-off-by: Brian Goff <cpuguy83@gmail.com>
| ... | ... |
@@ -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 |
+} |