mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
268a50c193
Fixes coder/internal#479 Fixes coder/internal#480
196 lines
4.6 KiB
Go
196 lines
4.6 KiB
Go
// Package watcher provides file system watching capabilities for the
|
|
// agent. It defines an interface for monitoring file changes and
|
|
// implementations that can be used to detect when configuration files
|
|
// are modified. This is primarily used to track changes to devcontainer
|
|
// configuration files and notify users when containers need to be
|
|
// recreated to apply the new configuration.
|
|
package watcher
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
var ErrClosed = xerrors.New("watcher closed")
|
|
|
|
// Watcher defines an interface for monitoring file system changes.
|
|
// Implementations track file modifications and provide an event stream
|
|
// that clients can consume to react to changes.
|
|
type Watcher interface {
|
|
// Add starts watching a file for changes.
|
|
Add(file string) error
|
|
|
|
// Remove stops watching a file for changes.
|
|
Remove(file string) error
|
|
|
|
// Next blocks until a file system event occurs or the context is canceled.
|
|
// It returns the next event or an error if the watcher encountered a problem.
|
|
Next(context.Context) (*fsnotify.Event, error)
|
|
|
|
// Close shuts down the watcher and releases any resources.
|
|
Close() error
|
|
}
|
|
|
|
type fsnotifyWatcher struct {
|
|
*fsnotify.Watcher
|
|
|
|
mu sync.Mutex // Protects following.
|
|
watchedFiles map[string]bool // Files being watched (absolute path -> bool).
|
|
watchedDirs map[string]int // Refcount of directories being watched (absolute path -> count).
|
|
closed bool // Protects closing of done.
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewFSNotify creates a new file system watcher that watches parent directories
|
|
// instead of individual files for more reliable event detection.
|
|
func NewFSNotify() (Watcher, error) {
|
|
w, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("create fsnotify watcher: %w", err)
|
|
}
|
|
return &fsnotifyWatcher{
|
|
Watcher: w,
|
|
done: make(chan struct{}),
|
|
watchedFiles: make(map[string]bool),
|
|
watchedDirs: make(map[string]int),
|
|
}, nil
|
|
}
|
|
|
|
func (f *fsnotifyWatcher) Add(file string) error {
|
|
absPath, err := filepath.Abs(file)
|
|
if err != nil {
|
|
return xerrors.Errorf("absolute path: %w", err)
|
|
}
|
|
|
|
dir := filepath.Dir(absPath)
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Already watching this file.
|
|
if f.closed || f.watchedFiles[absPath] {
|
|
return nil
|
|
}
|
|
|
|
// Start watching the parent directory if not already watching.
|
|
if f.watchedDirs[dir] == 0 {
|
|
if err := f.Watcher.Add(dir); err != nil {
|
|
return xerrors.Errorf("add directory to watcher: %w", err)
|
|
}
|
|
}
|
|
|
|
// Increment the reference count for this directory.
|
|
f.watchedDirs[dir]++
|
|
// Mark this file as watched.
|
|
f.watchedFiles[absPath] = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fsnotifyWatcher) Remove(file string) error {
|
|
absPath, err := filepath.Abs(file)
|
|
if err != nil {
|
|
return xerrors.Errorf("absolute path: %w", err)
|
|
}
|
|
|
|
dir := filepath.Dir(absPath)
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Not watching this file.
|
|
if f.closed || !f.watchedFiles[absPath] {
|
|
return nil
|
|
}
|
|
|
|
// Remove the file from our watch list.
|
|
delete(f.watchedFiles, absPath)
|
|
|
|
// Decrement the reference count for this directory.
|
|
f.watchedDirs[dir]--
|
|
|
|
// If no more files in this directory are being watched, stop
|
|
// watching the directory.
|
|
if f.watchedDirs[dir] <= 0 {
|
|
f.watchedDirs[dir] = 0 // Ensure non-negative count.
|
|
if err := f.Watcher.Remove(dir); err != nil {
|
|
return xerrors.Errorf("remove directory from watcher: %w", err)
|
|
}
|
|
delete(f.watchedDirs, dir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fsnotifyWatcher) Next(ctx context.Context) (event *fsnotify.Event, err error) {
|
|
defer func() {
|
|
if ctx.Err() != nil {
|
|
event = nil
|
|
err = ctx.Err()
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case evt, ok := <-f.Events:
|
|
if !ok {
|
|
return nil, ErrClosed
|
|
}
|
|
|
|
// Get the absolute path to match against our watched files.
|
|
absPath, err := filepath.Abs(evt.Name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
f.mu.Lock()
|
|
if f.closed {
|
|
f.mu.Unlock()
|
|
return nil, ErrClosed
|
|
}
|
|
isWatched := f.watchedFiles[absPath]
|
|
f.mu.Unlock()
|
|
if !isWatched {
|
|
continue // Ignore events for files not being watched.
|
|
}
|
|
|
|
return &evt, nil
|
|
|
|
case err, ok := <-f.Errors:
|
|
if !ok {
|
|
return nil, ErrClosed
|
|
}
|
|
return nil, xerrors.Errorf("watcher error: %w", err)
|
|
case <-f.done:
|
|
return nil, ErrClosed
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *fsnotifyWatcher) Close() (err error) {
|
|
f.mu.Lock()
|
|
f.watchedFiles = nil
|
|
f.watchedDirs = nil
|
|
closed := f.closed
|
|
f.closed = true
|
|
f.mu.Unlock()
|
|
|
|
if closed {
|
|
return ErrClosed
|
|
}
|
|
|
|
close(f.done)
|
|
|
|
if err := f.Watcher.Close(); err != nil {
|
|
return xerrors.Errorf("close watcher: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|