package watch
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"github.com/fsnotify/fsnotify"
"github.com/gitpod-io/gitpod/common-go/log"
)
type fileWatcher struct {
onChange func()
watcher *fsnotify.Watcher
hash string
}
func File(ctx context.Context, path string, onChange func()) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("unexpected error creating file watcher: %w", err)
}
fw := &fileWatcher{
watcher: watcher,
onChange: onChange,
}
hash, err := hashConfig(path)
if err != nil {
return fmt.Errorf("cannot get hash of file %v: %w", path, err)
}
watchDir, _ := filepath.Split(path)
err = watcher.Add(watchDir)
if err != nil {
watcher.Close()
return fmt.Errorf("unexpected error watching file %v: %w", path, err)
}
log.Infof("starting watch of file %v", path)
fw.hash = hash
go func() {
defer func() {
if err != nil {
log.WithError(err).Error("Stopping file watch")
} else {
log.Info("Stopping file watch")
}
err = watcher.Close()
if err != nil {
log.WithError(err).Error("Unexpected error closing file watcher")
}
}()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if !eventOpIs(event, fsnotify.Create) && !eventOpIs(event, fsnotify.Remove) {
continue
}
currentHash, err := hashConfig(path)
if err != nil {
log.WithError(err).WithField("event", event.Name).Warn("Cannot check if config has changed")
continue
}
if currentHash == fw.hash {
continue
}
log.WithField("path", path).Info("reloading file after change")
fw.hash = currentHash
fw.onChange()
case err := <-watcher.Errors:
log.WithError(err).Error("Unexpected error watching event")
case <-ctx.Done():
return
}
}
}()
return nil
}
func hashConfig(path string) (hash string, err error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func eventOpIs(event fsnotify.Event, op fsnotify.Op) bool {
return event.Op&op == op
}