mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
128 lines
2.7 KiB
Go
128 lines
2.7 KiB
Go
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License.AGPL.txt in the project root for license information.
|
|
|
|
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,
|
|
}
|
|
|
|
// initial hash of the file
|
|
hash, err := hashConfig(path)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot get hash of file %v: %w", path, err)
|
|
}
|
|
|
|
// visible files in a volume are symlinks to files in the writer's data directory.
|
|
// The files are stored in a hidden timestamped directory which is symlinked to by the data directory.
|
|
// The timestamped directory and data directory symlink are created in the writer's target dir.
|
|
// https://pkg.go.dev/k8s.io/kubernetes/pkg/volume/util#AtomicWriter
|
|
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() {
|
|
log.WithError(err).Error("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).Warn("Cannot check if config has changed")
|
|
return
|
|
}
|
|
|
|
// no change
|
|
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
|
|
}
|