Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WithEvents to filter events #523

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
124 changes: 91 additions & 33 deletions backend_fen.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,32 @@ type Watcher struct {
Events chan Event

// Errors sends any errors.
//
// [ErrEventOverflow] is used to indicate ther are too many events:
//
// - inotify: there are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; [WithBufferSize] can be used to increase it.
// - kqueue, fen: not used.
Errors chan error

mu sync.Mutex
port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]struct{} // Explicitly watched directories
watches map[string]struct{} // Explicitly watched non-directories
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]watch // Explicitly watched directories
watches map[string]watch // Explicitly watched non-directories
}

type watch struct {
events Op
}

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
w := &Watcher{
Events: make(chan Event),
Errors: make(chan error),
dirs: make(map[string]struct{}),
watches: make(map[string]struct{}),
dirs: make(map[string]watch),
watches: make(map[string]watch),
done: make(chan struct{}),
}

Expand Down Expand Up @@ -198,6 +208,8 @@ func (w *Watcher) Close() error {
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
Expand All @@ -215,14 +227,28 @@ func (w *Watcher) Close() error {
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
func (w *Watcher) Add(name string) error { return w.AddWith(name) }

// AddWith is like [Add], but has the possibility to add options. When using
// Add() the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
if w.port.PathIsWatched(name) {
return nil
}

with, err := getOptions(opts...)
if err != nil {
return err
}

// Currently we resolve symlinks that were explicitly requested to be
// watched. Otherwise we would use LStat here.
stat, err := os.Stat(name)
Expand All @@ -232,24 +258,24 @@ func (w *Watcher) Add(name string) error {

// Associate all files in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, true, w.associateFile)
err := w.handleDirectory(name, stat, true, with.events, w.associateFile)
if err != nil {
return err
}

w.mu.Lock()
w.dirs[name] = struct{}{}
w.dirs[name] = watch{events: with.events}
w.mu.Unlock()
return nil
}

err = w.associateFile(name, stat, true)
err = w.associateFile(name, stat, true, with.events)
if err != nil {
return err
}

w.mu.Lock()
w.watches[name] = struct{}{}
w.watches[name] = watch{events: with.events}
w.mu.Unlock()
return nil
}
Expand Down Expand Up @@ -283,7 +309,7 @@ func (w *Watcher) Remove(name string) error {

// Remove associations for every file in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, false, w.dissociateFile)
err := w.handleDirectory(name, stat, false, 0, w.dissociateFile)
if err != nil {
return err
}
Expand Down Expand Up @@ -345,7 +371,13 @@ func (w *Watcher) readEvents() {
}
}

func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
func (w *Watcher) handleDirectory(
path string,
stat os.FileInfo,
follow bool,
events Op,
handler func(string, os.FileInfo, bool, Op) error,
) error {
files, err := os.ReadDir(path)
if err != nil {
return err
Expand All @@ -357,21 +389,20 @@ func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, ha
if err != nil {
return err
}
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
err = handler(filepath.Join(path, finfo.Name()), finfo, false, events)
if err != nil {
return err
}
}

// And finally handle the directory itself.
return handler(path, stat, follow)
return handler(path, stat, follow, events)
}

// handleEvent might need to emit more than one fsnotify event
// if the events bitmap matches more than one event type
// (e.g. the file was both modified and had the
// attributes changed between when the association
// was created and the when event was returned)
// handleEvent might need to emit more than one fsnotify event if the events
// bitmap matches more than one event type (e.g. the file was both modified and
// had the attributes changed between when the association was created and the
// when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
var (
events = event.Events
Expand All @@ -381,11 +412,18 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
)

w.mu.Lock()
_, watchedDir := w.dirs[path]
_, watchedPath := w.watches[path]
watch, watchedDir := w.dirs[path]
var watchedPath bool
if !watchedDir {
watch, watchedPath = w.watches[path]
}
w.mu.Unlock()
isWatched := watchedDir || watchedPath

// TODO: probably want to filter some things based on WithEvents()
// (watch.events) here; there don't seem to be flags in FEN to control
// all of them.

if events&unix.FILE_DELETE != 0 {
if !w.sendEvent(path, Remove) {
return nil
Expand Down Expand Up @@ -491,7 +529,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
if stat != nil {
// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
return w.associateFile(path, stat, isWatched)
return w.associateFile(path, stat, isWatched, watch.events)
}
return nil
}
Expand All @@ -505,6 +543,8 @@ func (w *Watcher) updateDirectory(path string) error {
return err
}

watch := w.dirs[path]

for _, entry := range files {
path := filepath.Join(path, entry.Name())
if w.port.PathIsWatched(path) {
Expand All @@ -515,7 +555,7 @@ func (w *Watcher) updateDirectory(path string) error {
if err != nil {
return err
}
err = w.associateFile(path, finfo, false)
err = w.associateFile(path, finfo, false, watch.events)
if err != nil {
if !w.sendError(err) {
return nil
Expand All @@ -528,7 +568,7 @@ func (w *Watcher) updateDirectory(path string) error {
return nil
}

func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool, events Op) error {
if w.isClosed() {
return ErrClosed
}
Expand All @@ -553,18 +593,36 @@ func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) erro
return err
}
}
// FILE_NOFOLLOW means we watch symlinks themselves rather than their targets
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
if follow {
// We *DO* follow symlinks for explicitly watched entries
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB

var flags int
if events.Has(Create) {
// No flag.
}
if events.Has(Write) {
flags |= unix.FILE_MODIFIED
}
if events.Has(Remove) {
// Wasn't added before?
// flags |= FILE_DELETE
}
return w.port.AssociatePath(path, stat,
events,
stat.Mode())
if events.Has(Rename) {
// Wasn't added before?
//flags |= unix.FILE_RENAME_TO|unix.FILE_RENAME_FROM
}
if events.Has(Chmod) {
flags |= unix.FILE_ATTRIB
}

// FILE_NOFOLLOW means we watch symlinks themselves rather than their
// targets; only follow symlinks for explicitly watched entries.
if !follow {
flags |= unix.FILE_NOFOLLOW
}

return w.port.AssociatePath(path, stat, flags, stat.Mode())
}

func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool, _ Op) error {
if !w.port.PathIsWatched(path) {
return nil
}
Expand Down
48 changes: 43 additions & 5 deletions backend_inotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ type Watcher struct {
Events chan Event

// Errors sends any errors.
//
// [ErrEventOverflow] is used to indicate ther are too many events:
//
// - inotify: there are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; [WithBufferSize] can be used to increase it.
// - kqueue, fen: not used.
Errors chan error

// Store fd here as os.File.Read() will no longer return on close after
Expand Down Expand Up @@ -220,6 +226,8 @@ func (w *Watcher) Close() error {
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
Expand All @@ -237,15 +245,45 @@ func (w *Watcher) Close() error {
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
name = filepath.Clean(name)
func (w *Watcher) Add(name string) error { return w.AddWith(name) }

// AddWith is like [Add], but has the possibility to add options. When using
// Add() the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}

var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
name = filepath.Clean(name)
with, err := getOptions(opts...)
if err != nil {
return err
}

var flags uint32
if with.events.Has(Create) {
flags |= unix.IN_CREATE
}
if with.events.Has(Write) {
flags |= unix.IN_MODIFY
}
if with.events.Has(Remove) {
flags |= unix.IN_DELETE | unix.IN_DELETE_SELF
}
if with.events.Has(Rename) {
flags |= unix.IN_MOVED_FROM | unix.IN_MOVE_SELF
}
if with.events.Has(Rename) && with.events.Has(Create) {
flags |= unix.IN_MOVED_TO
}
if with.events.Has(Chmod) {
flags |= unix.IN_ATTRIB
}

w.mu.Lock()
defer w.mu.Unlock()
Expand Down