diff --git a/AUTHORS b/AUTHORS index 5ab5d41c..10fa97c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,11 +22,14 @@ Daniel Wagner-Hall Dave Cheney Evan Phoenix Francisco Souza +Gereon Frey Hari haran +Isaac Davis John C Barstow Kelvin Fo Ken-ichirou MATSUZAWA Matt Layher +Nahum Shalman Nathan Youngman Nickolai Zeldovich Patrick diff --git a/CHANGELOG.md b/CHANGELOG.md index be4d7ea2..2a848e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.5.0 / 2021-08-28 + +* Solaris: add support for File Event Notifications (fen) [#12](https://github.com/fsnotify/fsnotify/issues/12) + ## v1.4.7 / 2018-01-09 * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) diff --git a/LICENSE b/LICENSE index e180c8fb..4fc04b55 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. +Copyright (c) 2012-2021 fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/fen.go b/fen.go index ced39cb8..f6fb4a18 100644 --- a/fen.go +++ b/fen.go @@ -8,30 +8,285 @@ package fsnotify import ( "errors" + "fmt" + "golang.org/x/sys/unix" + "io/ioutil" + "os" + "path/filepath" ) // Watcher watches a set of files, delivering events to a channel. type Watcher struct { Events chan Event Errors chan error + + port *unix.EventPort + + done chan struct{} // Channel for sending a "quit message" to the reader goroutine + doneResp chan struct{} // Channel to respond to Close } // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { - return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") + var err error + + w := new(Watcher) + w.Events = make(chan Event) + w.Errors = make(chan error) + w.port, err = unix.NewEventPort() + if err != nil { + return nil, err + } + w.done = make(chan struct{}) + w.doneResp = make(chan struct{}) + + go w.readEvents() + return w, nil +} + +// sendEvent attempts to send an event to the user, returning true if the event +// was put in the channel successfully and false if the watcher has been closed. +func (w *Watcher) sendEvent(e Event) (sent bool) { + select { + case w.Events <- e: + return true + case <-w.done: + return false + } +} + +// sendError attempts to send an error to the user, returning true if the error +// was put in the channel successfully and false if the watcher has been closed. +func (w *Watcher) sendError(err error) (sent bool) { + select { + case w.Errors <- err: + return true + case <-w.done: + return false + } +} + +func (w *Watcher) isClosed() bool { + select { + case <-w.done: + return true + default: + return false + } } // Close removes all watches and closes the events channel. func (w *Watcher) Close() error { + if w.isClosed() { + return nil + } + close(w.done) + w.port.Close() + <-w.doneResp return nil } // Add starts watching the named file or directory (non-recursively). func (w *Watcher) Add(name string) error { - return nil + if w.isClosed() { + return errors.New("FEN watcher already closed") + } + if w.port.PathIsWatched(name) { + return nil + } + stat, err := os.Stat(name) + switch { + case err != nil: + return err + case stat.IsDir(): + return w.handleDirectory(name, stat, w.associateFile) + default: + return w.associateFile(name, stat) + } } // Remove stops watching the the named file or directory (non-recursively). func (w *Watcher) Remove(name string) error { + if w.isClosed() { + return errors.New("FEN watcher already closed") + } + if !w.port.PathIsWatched(name) { + return fmt.Errorf("can't remove non-existent FEN watch for: %s", name) + } + stat, err := os.Stat(name) + switch { + case err != nil: + return err + case stat.IsDir(): + return w.handleDirectory(name, stat, w.dissociateFile) + default: + return w.port.DissociatePath(name) + } +} + +// readEvents contains the main loop that runs in a goroutine watching for events. +func (w *Watcher) readEvents() { + // If this function returns, the watcher has been closed and we can + // close these channels + defer close(w.doneResp) + defer close(w.Errors) + defer close(w.Events) + + pevents := make([]unix.PortEvent, 8, 8) + for { + count, err := w.port.Get(pevents, 1, nil) + if err != nil { + // Interrupted system call (count should be 0) ignore and continue + if err == unix.EINTR && count == 0 { + continue + } + // Get failed because we called w.Close() + if err == unix.EBADF && w.isClosed() { + return + } + // There was an error not caused by calling w.Close() + if !w.sendError(err) { + return + } + } + + p := pevents[:count] + for _, pevent := range p { + if pevent.Source != unix.PORT_SOURCE_FILE { + // Event from unexpected source received; should never happen. + if !w.sendError(errors.New("Event from unexpected source received")) { + return + } + continue + } + + err = w.handleEvent(&pevent) + if err != nil { + if !w.sendError(err) { + return + } + } + } + } +} + +func (w *Watcher) handleDirectory(path string, stat os.FileInfo, handler func(string, os.FileInfo) error) error { + files, err := ioutil.ReadDir(path) + if err != nil { + return err + } + + // Handle all children of the directory. + for _, finfo := range files { + if !finfo.IsDir() { + err := handler(filepath.Join(path, finfo.Name()), finfo) + if err != nil { + return err + } + } + } + + // And finally handle the directory itself. + return handler(path, stat) +} + +func (w *Watcher) handleEvent(event *unix.PortEvent) error { + events := event.Events + path := event.Path + fmode := event.Cookie.(os.FileMode) + + var toSend *Event + reRegister := true + + switch { + case events&unix.FILE_MODIFIED == unix.FILE_MODIFIED: + if fmode.IsDir() { + if err := w.updateDirectory(path); err != nil { + return err + } + } else { + toSend = &Event{path, Write} + } + case events&unix.FILE_ATTRIB == unix.FILE_ATTRIB: + toSend = &Event{path, Chmod} + case events&unix.FILE_DELETE == unix.FILE_DELETE: + toSend = &Event{path, Remove} + reRegister = false + case events&unix.FILE_RENAME_FROM == unix.FILE_RENAME_FROM: + toSend = &Event{path, Rename} + // Don't keep watching the new file name + reRegister = false + case events&unix.FILE_RENAME_TO == unix.FILE_RENAME_TO: + // We don't report a Rename event for this case, because + // Rename events are interpreted as referring to the _old_ name + // of the file, and in this case the event would refer to the + // new name of the file. This type of rename event is not + // supported by fsnotify. + + // inotify reports a Remove event in this case, so we simulate + // this here. + toSend = &Event{path, Remove} + // Don't keep watching the file that was removed + reRegister = false + default: + return errors.New("unknown event received") + } + + if toSend != nil { + if !w.sendEvent(*toSend) { + return nil + } + } + if !reRegister { + return nil + } + + // If we get here, it means we've hit an event above that requires us to + // continue watching the file or directory + stat, err := os.Stat(path) + if err != nil { + return err + } + return w.associateFile(path, stat) +} + +func (w *Watcher) updateDirectory(path string) error { + // The directory was modified, so we must find unwatched entites and + // watch them. If something was removed from the directory, nothing will + // happen, as everything else should still be watched. + files, err := ioutil.ReadDir(path) + if err != nil { + return err + } + + for _, finfo := range files { + path := filepath.Join(path, finfo.Name()) + if w.port.PathIsWatched(path) { + continue + } + + err := w.associateFile(path, finfo) + if err != nil { + if !w.sendError(err) { + return nil + } + } + if !w.sendEvent(Event{path, Create}) { + return nil + } + } return nil } + +func (w *Watcher) associateFile(path string, stat os.FileInfo) error { + mode := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW + fmode := stat.Mode() + return w.port.AssociatePath(path, stat, mode, fmode) +} + +func (w *Watcher) dissociateFile(path string, stat os.FileInfo) error { + if !w.port.PathIsWatched(path) { + return nil + } + return w.port.DissociatePath(path) +} diff --git a/go.mod b/go.mod index ff11e13f..c887a368 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/fsnotify/fsnotify go 1.13 require golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 + +replace golang.org/x/sys => github.com/nshalman/sys v0.0.0-20210702004434-76c4cd66abf6 diff --git a/go.sum b/go.sum index f60af985..cf1c1843 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/nshalman/sys v0.0.0-20210702004434-76c4cd66abf6 h1:fsdjmo72FVG5tdcC+GZPTF7a+Z3tVtwlR/EQwh6dtIw= +github.com/nshalman/sys v0.0.0-20210702004434-76c4cd66abf6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/integration_test.go b/integration_test.go index 7096344d..90f26ddc 100644 --- a/integration_test.go +++ b/integration_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !plan9,!solaris +// +build !plan9 package fsnotify