From 4c338efd2813869a941298d7f4aaa2428012311e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 26 Sep 2024 11:16:35 +0000 Subject: [PATCH 1/5] Add renovate.json --- renovate.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} From 097c4b3d84867abf7292a82faf3fbec898830d36 Mon Sep 17 00:00:00 2001 From: Christian Schmied Date: Thu, 10 Oct 2024 16:12:29 +0200 Subject: [PATCH 2/5] implement in order of creation for oldFiles --- folderwatcher.go | 102 +++++++++++----- folderwatcher_globbing_test.go | 162 ++++++++++++++++++++++++++ folderwatcher_oldFilesInOrder_test.go | 87 ++++++++++++++ folderwatcher_test.go | 152 ------------------------ 4 files changed, 324 insertions(+), 179 deletions(-) create mode 100644 folderwatcher_globbing_test.go create mode 100644 folderwatcher_oldFilesInOrder_test.go diff --git a/folderwatcher.go b/folderwatcher.go index 596ff7e..e454da3 100644 --- a/folderwatcher.go +++ b/folderwatcher.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sync" "time" @@ -22,8 +23,10 @@ type FolderWatcher struct { quit chan struct{} mutex sync.Mutex wg sync.WaitGroup + debounceWg sync.WaitGroup debounceMap map[string]*time.Timer debouncedChan chan string + existingList []string enqueuedFiles int } @@ -71,6 +74,7 @@ func NewFolderWatcher(conf Config, includeExisting bool, quit chan struct{}) (*F quit: quit, debounceMap: make(map[string]*time.Timer), debouncedChan: make(chan string), + existingList: []string{}, } if includeExisting { @@ -81,10 +85,18 @@ func NewFolderWatcher(conf Config, includeExisting bool, quit chan struct{}) (*F } for _, entry := range entries { if !entry.IsDir() { - folderWatcher.foundFileEvent(filepath.Join(path, entry.Name())) + fullFilePath := filepath.Join(path, entry.Name()) + if folderWatcher.matchesPattern(fullFilePath) { + folderWatcher.existingList = append(folderWatcher.existingList, fullFilePath) + } } } } + slices.SortFunc(folderWatcher.existingList, func(a, b string) int { + infoA, _ := os.Stat(a) + infoB, _ := os.Stat(b) + return infoA.ModTime().Compare(infoB.ModTime()) + }) } return folderWatcher, nil @@ -95,21 +107,14 @@ type HandlerCallback func(path string, err error) func (w *FolderWatcher) Watch(handler FileHandler, callback HandlerCallback) { defer w.watcher.Close() - for stopped := false; !stopped; { - select { - case <-w.quit: - logger.Info("Stopping watcher") - stopped = true - case event, ok := <-w.watcher.Events: - if !ok { - stopped = true - break - } - logger.Debug("event:", event) - if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { - w.foundFileEvent(event.Name) - } - case file, _ := <-w.debouncedChan: + initialWait := sync.WaitGroup{} + + handlerWg := sync.WaitGroup{} + handlerWg.Add(1) + // Start File Handling + go func() { + defer handlerWg.Done() + for file := range w.debouncedChan { triggerCallback, err := handler(file) if err != nil { logger.Warn("error during handle:", err) @@ -117,7 +122,42 @@ func (w *FolderWatcher) Watch(handler FileHandler, callback HandlerCallback) { if triggerCallback { callback(file, err) } - w.wg.Done() + w.wg.Done() // mark enqueued file as done + } + }() + + if len(w.existingList) > 0 { + logger.Infof("got %d existing files", len(w.existingList)) + initialWait.Add(1) + go func() { + defer initialWait.Done() + for _, path := range w.existingList { + w.enqueue(path) + } + }() + } + + // Start Event Handling + for stopped := false; !stopped; { + select { + case <-w.quit: + logger.Info("Stopping watcher") + w.debounceWg.Wait() // wait for debounce to settle + w.wg.Wait() // wait for existing files to be handled + close(w.debouncedChan) //close the debounced chan to mark it as done + stopped = true + case event, ok := <-w.watcher.Events: + initialWait.Wait() // wait that all initial are enqueued, before handling of first event + if !ok { + stopped = true + break + } + logger.Debug("event:", event) + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { + if w.matchesPattern(event.Name) { + w.enqueueDebounce(event.Name) + } + } case err, ok := <-w.watcher.Errors: if !ok { stopped = true @@ -126,31 +166,39 @@ func (w *FolderWatcher) Watch(handler FileHandler, callback HandlerCallback) { logger.Warn("error:", err) } } - w.wg.Wait() + handlerWg.Wait() // wait for filehandler to be closed, implies debouncedChan is closed logger.Infof("watcher handled %d files", w.enqueuedFiles) } -func (w *FolderWatcher) foundFileEvent(fileName string) { +func (w *FolderWatcher) matchesPattern(fileName string) bool { baseName := filepath.Base(fileName) - if w.regex == nil || w.regex.MatchString(baseName) { - w.enqueueDebounce(fileName) + return w.regex == nil || w.regex.MatchString(baseName) +} + +func (w *FolderWatcher) enqueue(f string) { + w.wg.Add(1) + w.mutex.Lock() + defer w.mutex.Unlock() + if timer, ok := w.debounceMap[f]; ok && timer != nil { + timer.Stop() } + delete(w.debounceMap, f) + w.enqueuedFiles = w.enqueuedFiles + 1 + w.debouncedChan <- f } func (w *FolderWatcher) enqueueDebounce(f string) { + logger.Infof("Debounced Enqueue of %s", f) w.mutex.Lock() defer w.mutex.Unlock() if timer, ok := w.debounceMap[f]; ok && timer != nil { timer.Stop() } else { - w.wg.Add(1) + w.debounceWg.Add(1) } w.debounceMap[f] = time.AfterFunc(DebounceTime, func() { - w.mutex.Lock() - defer w.mutex.Unlock() - delete(w.debounceMap, f) - w.enqueuedFiles = w.enqueuedFiles + 1 - w.debouncedChan <- f + defer w.debounceWg.Done() + w.enqueue(f) }) } diff --git a/folderwatcher_globbing_test.go b/folderwatcher_globbing_test.go new file mode 100644 index 0000000..a6feb00 --- /dev/null +++ b/folderwatcher_globbing_test.go @@ -0,0 +1,162 @@ +package folderwatcher + +import ( + "context" + "os" + "path/filepath" + "strconv" + "sync" + "testing" + "time" +) + +func TestGlobbingN1(t *testing.T) { + tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpPath) + quitChan := make(chan struct{}) + + _ = os.MkdirAll(filepath.Join(tmpPath, "g0"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "g1"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "g2"), os.ModePerm) + + filesIn := []string{"A", "B", "C"} + var filesOut []string + + wg := sync.WaitGroup{} + + conf := Config{ + Folder: filepath.Join(tmpPath, "*"), + } + + watcher, err := NewFolderWatcher(conf, true, quitChan) + if err != nil { + t.Fatal(err) + } + + go watcher.Watch(func(filePath string) (bool, error) { + if !filepath.IsAbs(filePath) { + t.Errorf("file %s is not an absolute path", filePath) + } + rel, err := filepath.Rel(tmpPath, filePath) + if err != nil { + return true, err + } + filesOut = append(filesOut, rel) + wg.Done() + return true, nil + }, func(s string, err error) { + }) + + for i, f := range filesIn { + wg.Add(1) + _ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i), f), []byte{0}, os.ModePerm) + } + + finishedChan := make(chan struct{}) + go func() { + wg.Wait() + finishedChan <- struct{}{} + }() + + ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer ctxCancel() + + select { + case <-finishedChan: + case <-ctx.Done(): + t.Error(ctx.Err()) + } + quitChan <- struct{}{} + + if len(filesOut) != len(filesIn) { + t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn)) + } + for i, f := range filesIn { + if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i), f)) { + t.Errorf("File %s not found in %s", f, filesOut) + } + } +} + +func TestGlobbingN2(t *testing.T) { + tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpPath) + quitChan := make(chan struct{}) + + _ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s0"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s2"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s1"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s3"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(tmpPath, "gX"), os.ModePerm) + + filesIn := []string{"A", "B", "C", "D"} + var filesOut []string + + wg := sync.WaitGroup{} + + conf := Config{ + Folder: filepath.Join(tmpPath, "*", "*"), + } + + watcher, err := NewFolderWatcher(conf, true, quitChan) + if err != nil { + t.Fatal(err) + } + + go watcher.Watch(func(filePath string) (bool, error) { + if !filepath.IsAbs(filePath) { + t.Errorf("file %s is not an absolute path", filePath) + } + rel, err := filepath.Rel(tmpPath, filePath) + if err != nil { + return true, err + } + filesOut = append(filesOut, rel) + wg.Done() + return true, nil + }, func(s string, err error) { + }) + + for i, f := range filesIn { + wg.Add(1) + _ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f), []byte{0}, os.ModePerm) + } + + wg.Add(1) + _ = os.WriteFile(filepath.Join(tmpPath, "gX", "foo"), []byte{0}, os.ModePerm) + + finishedChan := make(chan struct{}) + go func() { + wg.Wait() + finishedChan <- struct{}{} + }() + + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + select { + case <-finishedChan: + case <-ctx.Done(): + wg.Done() // ctx Will Timeout, because gX/foo will always be remaining + // t.Error(ctx.Err()) + } + quitChan <- struct{}{} + + if len(filesOut) != len(filesIn) { + t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn)) + } + for i, f := range filesIn { + if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f)) { + t.Errorf("File %s not found in %s", f, filesOut) + } + } + if Contains(filesOut, filepath.Join("gX", "foo")) { + t.Errorf("File %s found in %s", filepath.Join("gX", "foo"), filesOut) + } +} diff --git a/folderwatcher_oldFilesInOrder_test.go b/folderwatcher_oldFilesInOrder_test.go new file mode 100644 index 0000000..da7bd1e --- /dev/null +++ b/folderwatcher_oldFilesInOrder_test.go @@ -0,0 +1,87 @@ +package folderwatcher + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestOldFilesInCreationOrder(t *testing.T) { + tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpPath) + quitChan := make(chan struct{}) + + filesIn := []string{"2", "C", "B", "A", "1"} + makeAbsFilePaths(t, tmpPath, &filesIn) + var filesOut []string + + wg := sync.WaitGroup{} + + conf := Config{ + Folder: tmpPath, + } + + for _, f := range filesIn { + wg.Add(1) + _ = os.WriteFile(f, []byte{0}, os.ModePerm) + time.Sleep(10 * time.Millisecond) + } + + watcher, err := NewFolderWatcher(conf, true, quitChan) + if err != nil { + t.Fatal(err) + } + + go watcher.Watch(func(filePath string) (bool, error) { + if !filepath.IsAbs(filePath) { + t.Errorf("file %s is not an absolute path", filePath) + } + filesOut = append(filesOut, filePath) + wg.Done() + return true, nil + }, func(s string, err error) { + }) + + finishedChan := make(chan struct{}) + go func() { + wg.Wait() + finishedChan <- struct{}{} + }() + + ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer ctxCancel() + + select { + case <-finishedChan: + case <-ctx.Done(): + t.Error(ctx.Err()) + } + quitChan <- struct{}{} + + if len(filesOut) != len(filesIn) { + t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn)) + } + + filesInS := fmt.Sprintf("%s", filesIn) + filesOutS := fmt.Sprintf("%s", filesOut) + if filesInS != filesOutS { + t.Errorf("filesIn != filesOut %s != %s", filesIn, filesOut) + } +} + +func makeAbsFilePaths(t *testing.T, rootPath string, filesIn *[]string) { + t.Helper() + for i, f := range *filesIn { //remap filesIn to Full Path + (*filesIn)[i] = filepath.Join(rootPath, f) + if !filepath.IsAbs((*filesIn)[i]) { + t.Fatalf("file %s is not an absolute path", (*filesIn)[i]) + } + } +} diff --git a/folderwatcher_test.go b/folderwatcher_test.go index ab842ed..6e3543a 100644 --- a/folderwatcher_test.go +++ b/folderwatcher_test.go @@ -4,7 +4,6 @@ import ( "context" "os" "path/filepath" - "strconv" "sync" "testing" "time" @@ -314,157 +313,6 @@ func TestOldAndNewFileHandlingWithoutExisting(t *testing.T) { } } -func TestGlobbingN1(t *testing.T) { - tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpPath) - quitChan := make(chan struct{}) - - _ = os.MkdirAll(filepath.Join(tmpPath, "g0"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "g1"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "g2"), os.ModePerm) - - filesIn := []string{"A", "B", "C"} - var filesOut []string - - wg := sync.WaitGroup{} - - conf := Config{ - Folder: filepath.Join(tmpPath, "*"), - } - - watcher, err := NewFolderWatcher(conf, true, quitChan) - if err != nil { - t.Fatal(err) - } - - go watcher.Watch(func(filePath string) (bool, error) { - if !filepath.IsAbs(filePath) { - t.Errorf("file %s is not an absolute path", filePath) - } - rel, err := filepath.Rel(tmpPath, filePath) - if err != nil { - return true, err - } - filesOut = append(filesOut, rel) - wg.Done() - return true, nil - }, func(s string, err error) { - }) - - for i, f := range filesIn { - wg.Add(1) - _ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i), f), []byte{0}, os.ModePerm) - } - - finishedChan := make(chan struct{}) - go func() { - wg.Wait() - finishedChan <- struct{}{} - }() - - ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer ctxCancel() - - select { - case <-finishedChan: - case <-ctx.Done(): - t.Error(ctx.Err()) - } - quitChan <- struct{}{} - - if len(filesOut) != len(filesIn) { - t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn)) - } - for i, f := range filesIn { - if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i), f)) { - t.Errorf("File %s not found in %s", f, filesOut) - } - } -} - -func TestGlobbingN2(t *testing.T) { - tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpPath) - quitChan := make(chan struct{}) - - _ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s0"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s2"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s1"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s3"), os.ModePerm) - _ = os.MkdirAll(filepath.Join(tmpPath, "gX"), os.ModePerm) - - filesIn := []string{"A", "B", "C", "D"} - var filesOut []string - - wg := sync.WaitGroup{} - - conf := Config{ - Folder: filepath.Join(tmpPath, "*", "*"), - } - - watcher, err := NewFolderWatcher(conf, true, quitChan) - if err != nil { - t.Fatal(err) - } - - go watcher.Watch(func(filePath string) (bool, error) { - if !filepath.IsAbs(filePath) { - t.Errorf("file %s is not an absolute path", filePath) - } - rel, err := filepath.Rel(tmpPath, filePath) - if err != nil { - return true, err - } - filesOut = append(filesOut, rel) - wg.Done() - return true, nil - }, func(s string, err error) { - }) - - for i, f := range filesIn { - wg.Add(1) - _ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f), []byte{0}, os.ModePerm) - } - - wg.Add(1) - _ = os.WriteFile(filepath.Join(tmpPath, "gX", "foo"), []byte{0}, os.ModePerm) - - finishedChan := make(chan struct{}) - go func() { - wg.Wait() - finishedChan <- struct{}{} - }() - - ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer ctxCancel() - - select { - case <-finishedChan: - case <-ctx.Done(): - wg.Done() // ctx Will Timeout, because gX/foo will always be remaining - // t.Error(ctx.Err()) - } - quitChan <- struct{}{} - - if len(filesOut) != len(filesIn) { - t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn)) - } - for i, f := range filesIn { - if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f)) { - t.Errorf("File %s not found in %s", f, filesOut) - } - } - if Contains(filesOut, filepath.Join("gX", "foo")) { - t.Errorf("File %s found in %s", filepath.Join("gX", "foo"), filesOut) - } -} - func Contains[T comparable](slice []T, item T) bool { for _, v := range slice { if v == item { From e4852b7cc743eef7a08555bc623b40e69047de1d Mon Sep 17 00:00:00 2001 From: Christian Schmied Date: Mon, 28 Oct 2024 11:20:58 +0100 Subject: [PATCH 3/5] add error when matchedFolders is empty --- folderwatcher.go | 12 ++++++++++++ folderwatcher_notExistingFolder_test.go | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 folderwatcher_notExistingFolder_test.go diff --git a/folderwatcher.go b/folderwatcher.go index e454da3..6e4203f 100644 --- a/folderwatcher.go +++ b/folderwatcher.go @@ -1,6 +1,7 @@ package folderwatcher import ( + "fmt" "github.com/sirupsen/logrus" "os" "path/filepath" @@ -52,7 +53,18 @@ func NewFolderWatcher(conf Config, includeExisting bool, quit chan struct{}) (*F } } + if len(matchedFolders) == 0 { + return nil, fmt.Errorf("no folders found matching '%s'", conf.Folder) + } + for _, path := range matchedFolders { + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + if !stat.IsDir() { + return nil, fmt.Errorf("%s is not a folder", path) + } err = watcher.Add(path) if err != nil { return nil, err diff --git a/folderwatcher_notExistingFolder_test.go b/folderwatcher_notExistingFolder_test.go new file mode 100644 index 0000000..8cbdce8 --- /dev/null +++ b/folderwatcher_notExistingFolder_test.go @@ -0,0 +1,22 @@ +package folderwatcher + +import ( + "os" + "testing" +) + +func TestNotExistingFolder(t *testing.T) { + tmpPath, err := os.MkdirTemp("", ".folderwatcher_test") + if err != nil { + t.Fatal(err) + } + _ = os.RemoveAll(tmpPath) + conf := Config{ + Folder: tmpPath, + } + + _, err = NewFolderWatcher(conf, true, nil) + if err == nil { + t.Fatal("expected error") + } +} From 7a8c27dc4efdb1b7c653dd138cf3dbf8f68060ce Mon Sep 17 00:00:00 2001 From: Christian Schmied Date: Tue, 29 Oct 2024 10:59:33 +0100 Subject: [PATCH 4/5] set debounce enqueue log to debug --- folderwatcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folderwatcher.go b/folderwatcher.go index 6e4203f..73f0999 100644 --- a/folderwatcher.go +++ b/folderwatcher.go @@ -200,7 +200,7 @@ func (w *FolderWatcher) enqueue(f string) { } func (w *FolderWatcher) enqueueDebounce(f string) { - logger.Infof("Debounced Enqueue of %s", f) + logger.Debugf("Debounced Enqueue of %s", f) w.mutex.Lock() defer w.mutex.Unlock() From a0e3d487419939ad20d82846f7882ac38199e12f Mon Sep 17 00:00:00 2001 From: Christian Schmied Date: Thu, 14 Nov 2024 12:37:49 +0100 Subject: [PATCH 5/5] bump deps, use go 1.23.3 --- Jenkinsfile | 2 +- go.mod | 6 +++--- go.sum | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7b6efd5..cbb307f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,7 +8,7 @@ pipeline { agent { docker { - image 'docker.sportwanninger.de/infrastructure/golang-mingw:1.23.1' + image 'docker.sportwanninger.de/infrastructure/golang-mingw:1.23.3' args "${dindArgs()}" } } diff --git a/go.mod b/go.mod index 8ad4eda..8059258 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module git.sportwanninger.de/go/folderwatcher -go 1.23.1 +go 1.23.3 require ( - github.com/fsnotify/fsnotify v1.7.0 + github.com/fsnotify/fsnotify v1.8.0 github.com/sirupsen/logrus v1.9.3 ) -require golang.org/x/sys v0.4.0 // indirect +require golang.org/x/sys v0.27.0 // indirect diff --git a/go.sum b/go.sum index f2cbafb..da3c7e2 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -11,8 +11,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=