-
Notifications
You must be signed in to change notification settings - Fork 47
/
watcher.go
137 lines (118 loc) · 2.89 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
/*
Copyright (c) 2021 - Present. Blend Labs, Inc. All rights reserved
Use of this source code is governed by a MIT license that can be found in the LICENSE file.
*/
package fileutil
import (
"os"
"time"
"github.com/blend/go-sdk/async"
"github.com/blend/go-sdk/ex"
)
// Watch constants
const (
ErrWatchStopped ex.Class = "watch file should stop"
DefaultWatchPollInterval = 500 * time.Millisecond
)
// Watch watches a file for changes and calls the action if there are changes.
// It does this by polling the file for ModTime changes every 500ms.
// It is not designed for watching a large number of files.
// This function blocks, and you should probably call this with its own goroutine.
// The action takes a direct file handle, and is _NOT_ responsible for closing
// the file; the watcher will do that when the action has completed.
func Watch(path string, action WatchAction) error {
errors := make(chan error, 1)
w := NewWatcher(path, action)
w.Errors = errors
w.Starting()
w.Watch()
if len(errors) > 0 {
return <-errors
}
return nil
}
// NewWatcher returns a new watcher.
func NewWatcher(path string, action WatchAction, opts ...WatcherOption) *Watcher {
watch := Watcher{
Latch: async.NewLatch(),
Path: path,
Action: action,
}
for _, opt := range opts {
opt(&watch)
}
return &watch
}
// WatchAction is an action for the file watcher.
type WatchAction func(*os.File) error
// WatcherOption is an option for a watcher.
type WatcherOption func(*Watcher)
// Watcher watches a file for changes and calls the action.
type Watcher struct {
*async.Latch
Path string
PollInterval time.Duration
Action func(*os.File) error
Errors chan error
}
// PollIntervalOrDefault returns the polling interval or a default.
func (w Watcher) PollIntervalOrDefault() time.Duration {
if w.PollInterval > 0 {
return w.PollInterval
}
return DefaultWatchPollInterval
}
// Watch watches a given file.
func (w Watcher) Watch() {
stat, err := os.Stat(w.Path)
if err != nil {
w.handleError(ex.New(err))
return
}
w.Started()
lastMod := stat.ModTime()
ticker := time.NewTicker(w.PollIntervalOrDefault())
defer ticker.Stop()
for {
select {
case <-ticker.C:
stat, err = os.Stat(w.Path)
if err != nil {
w.handleError(ex.New(err))
return
}
if stat.ModTime().After(lastMod) {
file, err := os.Open(w.Path)
if err != nil {
w.handleError(ex.New(err))
return
}
// call the action
// and no matter what, close the file.
func() {
defer file.Close()
err = w.Action(file)
}()
if err != nil {
if ex.Is(err, ErrWatchStopped) {
return
}
w.handleError(ex.New(err))
return
}
lastMod = stat.ModTime()
}
case <-w.NotifyStopping():
w.Stopped()
return
}
}
}
func (w Watcher) handleError(err error) {
if err == nil {
return
}
if w.Errors != nil {
w.Errors <- err
}
}