Просмотр исходного кода

fix: watcher pattern matching and retrying (#1143)

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Alexander Stecher 4 месяцев назад
Родитель
Сommit
56d5d50ea9

+ 7 - 4
internal/watcher/watch_pattern.go

@@ -10,9 +10,10 @@ import (
 )
 
 type watchPattern struct {
-	dir      string
-	patterns []string
-	trigger  chan struct{}
+	dir          string
+	patterns     []string
+	trigger      chan struct{}
+	failureCount int
 }
 
 func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) {
@@ -90,7 +91,9 @@ func isValidPattern(fileName string, dir string, patterns []string) bool {
 	if !strings.HasPrefix(fileName, dir) {
 		return false
 	}
-	fileNameWithoutDir := strings.TrimLeft(fileName, dir+"/")
+
+	// remove the dir and '/' from the filename
+	fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, dir), "/")
 
 	// if the pattern has size 1 we can match it directly against the filename
 	if len(patterns) == 1 {

+ 7 - 2
internal/watcher/watch_pattern_test.go

@@ -36,6 +36,7 @@ func TestWatchesCorrectDir(t *testing.T) {
 	hasDir(t, "/path/*.php", "/path")
 	hasDir(t, "/path/*/*.php", "/path")
 	hasDir(t, "/path/?dir/*.php", "/path")
+	hasDir(t, "/path/{dir1,dir2}/**/*.php", "/path")
 	hasDir(t, ".", relativeDir(t, ""))
 	hasDir(t, "./", relativeDir(t, ""))
 	hasDir(t, "./**", relativeDir(t, ""))
@@ -130,14 +131,18 @@ func TestValidExtendedPatterns(t *testing.T) {
 	shouldMatch(t, "/path/*.{php,twig}", "/path/file.php")
 	shouldMatch(t, "/path/*.{php,twig}", "/path/file.twig")
 	shouldMatch(t, "/path/**/{file.php,file.twig}", "/path/subpath/file.twig")
-	shouldMatch(t, "/path/{folder1,folder2}/file.php", "/path/folder1/file.php")
+	shouldMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir1/file.php")
+	shouldMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir2/file.php")
+	shouldMatch(t, "/app/{app,config,resources}/**/*.php", "/app/app/subpath/file.php")
+	shouldMatch(t, "/app/{app,config,resources}/**/*.php", "/app/config/subpath/file.php")
 }
 
 func TestInValidExtendedPatterns(t *testing.T) {
 	shouldNotMatch(t, "/path/*.{php}", "/path/file.txt")
 	shouldNotMatch(t, "/path/*.{php,twig}", "/path/file.txt")
 	shouldNotMatch(t, "/path/{file.php,file.twig}", "/path/file.txt")
-	shouldNotMatch(t, "/path/{folder1,folder2}/file.php", "/path/folder3/file.php")
+	shouldNotMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir3/file.php")
+	shouldNotMatch(t, "/path/{dir1,dir2}/**/*.php", "/path/dir1/subpath/file.txt")
 }
 
 func relativeDir(t *testing.T, relativePath string) string {

+ 47 - 3
internal/watcher/watcher.go

@@ -10,7 +10,9 @@ import "C"
 import (
 	"errors"
 	"runtime/cgo"
+	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
 	"unsafe"
 
@@ -27,6 +29,13 @@ type watcher struct {
 // duration to wait before triggering a reload after a file change
 const debounceDuration = 150 * time.Millisecond
 
+// times to retry watching if the watcher was closed prematurely
+const maxFailureCount = 5
+const failureResetDuration = 5 * time.Second
+
+var failureMu = sync.Mutex{}
+var watcherIsActive = atomic.Bool{}
+
 var (
 	// the currently active file watcher
 	activeWatcher *watcher
@@ -42,9 +51,10 @@ func InitWatcher(filePatterns []string, callback func(), zapLogger *zap.Logger)
 	if len(filePatterns) == 0 {
 		return nil
 	}
-	if activeWatcher != nil {
+	if watcherIsActive.Load() {
 		return AlreadyStartedError
 	}
+	watcherIsActive.Store(true)
 	logger = zapLogger
 	activeWatcher = &watcher{callback: callback}
 	err := activeWatcher.startWatching(filePatterns)
@@ -57,15 +67,42 @@ func InitWatcher(filePatterns []string, callback func(), zapLogger *zap.Logger)
 }
 
 func DrainWatcher() {
-	if activeWatcher == nil {
+	if !watcherIsActive.Load() {
 		return
 	}
+	watcherIsActive.Store(false)
 	logger.Debug("stopping watcher")
 	activeWatcher.stopWatching()
 	reloadWaitGroup.Wait()
 	activeWatcher = nil
 }
 
+// TODO: how to test this?
+func retryWatching(watchPattern *watchPattern) {
+	failureMu.Lock()
+	defer failureMu.Unlock()
+	if watchPattern.failureCount >= maxFailureCount {
+		return
+	}
+	logger.Info("watcher was closed prematurely, retrying...", zap.String("dir", watchPattern.dir))
+
+	watchPattern.failureCount++
+	session, err := startSession(watchPattern)
+	if err != nil {
+		activeWatcher.sessions = append(activeWatcher.sessions, session)
+	}
+
+	// reset the failure-count if the watcher hasn't reached max failures after 5 seconds
+	go func() {
+		time.Sleep(failureResetDuration * time.Second)
+		failureMu.Lock()
+		if watchPattern.failureCount < maxFailureCount {
+			watchPattern.failureCount = 0
+		}
+		failureMu.Unlock()
+	}()
+}
+
 func (w *watcher) startWatching(filePatterns []string) error {
 	w.trigger = make(chan struct{})
 	w.stop = make(chan struct{})
@@ -117,9 +154,16 @@ func stopSession(session C.uintptr_t) {
 //export go_handle_file_watcher_event
 func go_handle_file_watcher_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) {
 	watchPattern := cgo.Handle(handle).Value().(*watchPattern)
-	if watchPattern.allowReload(C.GoString(path), int(eventType), int(pathType)) {
+	goPath := C.GoString(path)
+
+	if watchPattern.allowReload(goPath, int(eventType), int(pathType)) {
 		watchPattern.trigger <- struct{}{}
 	}
+
+	// If the watcher prematurely sends the die@ event, retry watching
+	if pathType == 4 && strings.HasPrefix(goPath, "e/self/die@") && watcherIsActive.Load() {
+		retryWatching(watchPattern)
+	}
 }
 
 func listenForFileEvents(triggerWatcher chan struct{}, stopWatcher chan struct{}) {