Skip to content

Commit 577892f

Browse files
aledbfroboquat
authored andcommitted
[common-go] Add file watcher
1 parent b06842b commit 577892f

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

components/common-go/watch/file.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package watch
6+
7+
import (
8+
"context"
9+
"crypto/sha256"
10+
"encoding/hex"
11+
"io"
12+
"os"
13+
"path/filepath"
14+
15+
"github.com/fsnotify/fsnotify"
16+
"golang.org/x/xerrors"
17+
18+
"github.com/gitpod-io/gitpod/common-go/log"
19+
)
20+
21+
type fileWatcher struct {
22+
onChange func()
23+
24+
watcher *fsnotify.Watcher
25+
26+
hash string
27+
}
28+
29+
func File(ctx context.Context, path string, onChange func()) error {
30+
watcher, err := fsnotify.NewWatcher()
31+
if err != nil {
32+
return xerrors.Errorf("unexpected error creating file watcher: %w", err)
33+
}
34+
35+
fw := &fileWatcher{
36+
watcher: watcher,
37+
onChange: onChange,
38+
}
39+
40+
// initial hash of the file
41+
hash, err := hashConfig(path)
42+
if err != nil {
43+
return xerrors.Errorf("cannot get hash of file %v: %w", path, err)
44+
}
45+
46+
// visible files in a volume are symlinks to files in the writer's data directory.
47+
// The files are stored in a hidden timestamped directory which is symlinked to by the data directory.
48+
// The timestamped directory and data directory symlink are created in the writer's target dir.
49+
// https://pkg.go.dev/k8s.io/kubernetes/pkg/volume/util#AtomicWriter
50+
watchDir, _ := filepath.Split(path)
51+
err = watcher.Add(watchDir)
52+
if err != nil {
53+
watcher.Close()
54+
return xerrors.Errorf("unexpected error watching file %v: %w", path, err)
55+
}
56+
57+
log.Infof("starting watch of file %v", path)
58+
59+
fw.hash = hash
60+
61+
go func() {
62+
defer func() {
63+
log.WithError(err).Error("stopping file watch")
64+
65+
err = watcher.Close()
66+
if err != nil {
67+
log.WithError(err).Error("unexpected error closing file watcher")
68+
}
69+
}()
70+
71+
for {
72+
select {
73+
case event, ok := <-watcher.Events:
74+
if !ok {
75+
return
76+
}
77+
78+
if !eventOpIs(event, fsnotify.Create) && !eventOpIs(event, fsnotify.Remove) {
79+
continue
80+
}
81+
82+
currentHash, err := hashConfig(path)
83+
if err != nil {
84+
log.WithError(err).Warn("cannot check if config has changed")
85+
return
86+
}
87+
88+
// no change
89+
if currentHash == fw.hash {
90+
continue
91+
}
92+
93+
log.WithField("path", path).Info("reloading file after change")
94+
95+
fw.hash = currentHash
96+
fw.onChange()
97+
case err := <-watcher.Errors:
98+
log.WithError(err).Error("unexpected error watching event")
99+
case <-ctx.Done():
100+
return
101+
}
102+
}
103+
}()
104+
105+
return nil
106+
}
107+
108+
func hashConfig(path string) (hash string, err error) {
109+
f, err := os.Open(path)
110+
if err != nil {
111+
return "", err
112+
}
113+
defer f.Close()
114+
115+
h := sha256.New()
116+
117+
_, err = io.Copy(h, f)
118+
if err != nil {
119+
return "", err
120+
}
121+
122+
return hex.EncodeToString(h.Sum(nil)), nil
123+
}
124+
125+
func eventOpIs(event fsnotify.Event, op fsnotify.Op) bool {
126+
return event.Op&op == op
127+
}

0 commit comments

Comments
 (0)