package file
import (
"context"
"encoding"
"fmt"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
)
type Detector int
const (
DetectorInvalid Detector = iota
DetectorFSNotify
DetectorPoll
DetectorDefault = DetectorFSNotify
)
var (
_ encoding.TextMarshaler = Detector(0)
_ encoding.TextUnmarshaler = (*Detector)(nil)
)
func (ut Detector) String() string {
switch ut {
case DetectorFSNotify:
return "fsnotify"
case DetectorPoll:
return "poll"
default:
return fmt.Sprintf("Detector(%d)", ut)
}
}
func (ut Detector) MarshalText() (text []byte, err error) {
return []byte(ut.String()), nil
}
func (ut *Detector) UnmarshalText(text []byte) error {
switch string(text) {
case "":
*ut = DetectorDefault
case "fsnotify":
*ut = DetectorFSNotify
case "poll":
*ut = DetectorPoll
default:
return fmt.Errorf("unrecognized detector %q, expected fsnotify or poll", string(text))
}
return nil
}
type fsNotify struct {
opts fsNotifyOptions
cancel context.CancelFunc
watcherMut sync.Mutex
watcher *fsnotify.Watcher
}
type fsNotifyOptions struct {
Logger log.Logger
Filename string
ReloadFile func()
PollFrequency time.Duration
}
func newFSNotify(opts fsNotifyOptions) (*fsNotify, error) {
w, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
if err := w.Add(opts.Filename); err != nil {
level.Warn(opts.Logger).Log("msg", "failed to watch file", "err", err)
}
ctx, cancel := context.WithCancel(context.Background())
wd := &fsNotify{
opts: opts,
watcher: w,
cancel: cancel,
}
go wd.wait(ctx)
return wd, nil
}
func (fsn *fsNotify) wait(ctx context.Context) {
pollTick := time.NewTicker(fsn.opts.PollFrequency)
defer pollTick.Stop()
for {
select {
case <-ctx.Done():
return
case <-pollTick.C:
fsn.watcherMut.Lock()
err := fsn.watcher.Add(fsn.opts.Filename)
fsn.watcherMut.Unlock()
if err != nil {
level.Warn(fsn.opts.Logger).Log("msg", "failed re-watch file", "err", err)
}
fsn.opts.ReloadFile()
case err := <-fsn.watcher.Errors:
if err != nil {
level.Warn(fsn.opts.Logger).Log("msg", "got error from fsnotify watcher; treating as file updated event", "err", err)
fsn.opts.ReloadFile()
}
case ev := <-fsn.watcher.Events:
level.Debug(fsn.opts.Logger).Log("msg", "got fsnotify event", "op", ev.Op.String())
fsn.opts.ReloadFile()
}
}
}
func (fsn *fsNotify) Close() error {
fsn.watcherMut.Lock()
defer fsn.watcherMut.Unlock()
fsn.cancel()
return fsn.watcher.Close()
}
type poller struct {
opts pollerOptions
cancel context.CancelFunc
}
type pollerOptions struct {
Filename string
ReloadFile func()
PollFrequency time.Duration
}
func newPoller(opts pollerOptions) *poller {
ctx, cancel := context.WithCancel(context.Background())
pw := &poller{
opts: opts,
cancel: cancel,
}
go pw.run(ctx)
return pw
}
func (p *poller) run(ctx context.Context) {
t := time.NewTicker(p.opts.PollFrequency)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
p.opts.ReloadFile()
}
}
}
func (p *poller) Close() error {
p.cancel()
return nil
}