chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// Package watcher provides file watching for hot reload
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event represents a file change event
|
||||
type Event struct {
|
||||
Path string
|
||||
Dir string // The service directory that was affected
|
||||
}
|
||||
|
||||
// Watcher watches directories for file changes
|
||||
type Watcher struct {
|
||||
dirs []string
|
||||
events chan Event
|
||||
done chan struct{}
|
||||
interval time.Duration
|
||||
debounce time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
modTimes map[string]time.Time
|
||||
}
|
||||
|
||||
// Option configures the watcher
|
||||
type Option func(*Watcher)
|
||||
|
||||
// WithInterval sets the polling interval
|
||||
func WithInterval(d time.Duration) Option {
|
||||
return func(w *Watcher) {
|
||||
w.interval = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithDebounce sets the debounce duration for rapid changes
|
||||
func WithDebounce(d time.Duration) Option {
|
||||
return func(w *Watcher) {
|
||||
w.debounce = d
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new file watcher for the given directories
|
||||
func New(dirs []string, opts ...Option) *Watcher {
|
||||
w := &Watcher{
|
||||
dirs: dirs,
|
||||
events: make(chan Event, 100),
|
||||
done: make(chan struct{}),
|
||||
interval: 500 * time.Millisecond,
|
||||
debounce: 300 * time.Millisecond,
|
||||
modTimes: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(w)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Events returns the channel of file change events
|
||||
func (w *Watcher) Events() <-chan Event {
|
||||
return w.events
|
||||
}
|
||||
|
||||
// Start begins watching for file changes
|
||||
func (w *Watcher) Start() {
|
||||
// Initial scan to populate mod times
|
||||
w.scan(false)
|
||||
|
||||
go w.watch()
|
||||
}
|
||||
|
||||
// AddDir adds a new directory to watch
|
||||
func (w *Watcher) AddDir(dir string) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
for _, d := range w.dirs {
|
||||
if d == dir {
|
||||
return
|
||||
}
|
||||
}
|
||||
w.dirs = append(w.dirs, dir)
|
||||
}
|
||||
|
||||
// Dirs returns the currently watched directories
|
||||
func (w *Watcher) Dirs() []string {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return append([]string{}, w.dirs...)
|
||||
}
|
||||
|
||||
// Stop stops the watcher
|
||||
func (w *Watcher) Stop() {
|
||||
close(w.done)
|
||||
}
|
||||
|
||||
func (w *Watcher) watch() {
|
||||
ticker := time.NewTicker(w.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Track pending events per directory for debouncing
|
||||
pending := make(map[string]time.Time)
|
||||
var pendingMu sync.Mutex
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
changed := w.scan(true)
|
||||
now := time.Now()
|
||||
|
||||
pendingMu.Lock()
|
||||
for _, dir := range changed {
|
||||
pending[dir] = now
|
||||
}
|
||||
|
||||
// Emit events for directories that have been stable
|
||||
for dir, t := range pending {
|
||||
if now.Sub(t) >= w.debounce {
|
||||
select {
|
||||
case w.events <- Event{Dir: dir}:
|
||||
default:
|
||||
// Channel full, skip
|
||||
}
|
||||
delete(pending, dir)
|
||||
}
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) scan(notify bool) []string {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
var changed []string
|
||||
changedDirs := make(map[string]bool)
|
||||
|
||||
for _, dir := range w.dirs {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip hidden directories and vendor
|
||||
if info.IsDir() {
|
||||
name := info.Name()
|
||||
if strings.HasPrefix(name, ".") || name == "vendor" || name == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only watch .go files
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
modTime := info.ModTime()
|
||||
if oldTime, exists := w.modTimes[path]; exists {
|
||||
if modTime.After(oldTime) && notify {
|
||||
if !changedDirs[absDir] {
|
||||
changedDirs[absDir] = true
|
||||
changed = append(changed, absDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.modTimes[path] = modTime
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
Reference in New Issue
Block a user