| //go:build solaris |
| |
| // FEN backend for illumos (supported) and Solaris (untested, but should work). |
| // |
| // See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create |
| |
| package fsnotify |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "sync" |
| "time" |
| |
| "github.com/fsnotify/fsnotify/internal" |
| "golang.org/x/sys/unix" |
| ) |
| |
| type fen struct { |
| Events chan Event |
| 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]Op // Explicitly watched directories |
| watches map[string]Op // Explicitly watched non-directories |
| } |
| |
| func newBackend(ev chan Event, errs chan error) (backend, error) { |
| return newBufferedBackend(0, ev, errs) |
| } |
| |
| func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { |
| w := &fen{ |
| Events: ev, |
| Errors: errs, |
| dirs: make(map[string]Op), |
| watches: make(map[string]Op), |
| done: make(chan struct{}), |
| } |
| |
| var err error |
| w.port, err = unix.NewEventPort() |
| if err != nil { |
| return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err) |
| } |
| |
| 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 *fen) sendEvent(name string, op Op) (sent bool) { |
| select { |
| case <-w.done: |
| return false |
| case w.Events <- Event{Name: name, Op: op}: |
| return true |
| } |
| } |
| |
| // 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 *fen) sendError(err error) (sent bool) { |
| if err == nil { |
| return true |
| } |
| select { |
| case <-w.done: |
| return false |
| case w.Errors <- err: |
| return true |
| } |
| } |
| |
| func (w *fen) isClosed() bool { |
| select { |
| case <-w.done: |
| return true |
| default: |
| return false |
| } |
| } |
| |
| func (w *fen) Close() error { |
| // Take the lock used by associateFile to prevent lingering events from |
| // being processed after the close |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| if w.isClosed() { |
| return nil |
| } |
| close(w.done) |
| return w.port.Close() |
| } |
| |
| func (w *fen) Add(name string) error { return w.AddWith(name) } |
| |
| func (w *fen) AddWith(name string, opts ...addOpt) error { |
| if w.isClosed() { |
| return ErrClosed |
| } |
| if debug { |
| fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", |
| time.Now().Format("15:04:05.000000000"), name) |
| } |
| |
| with := getOptions(opts...) |
| if !w.xSupports(with.op) { |
| return fmt.Errorf("%w: %s", xErrUnsupported, with.op) |
| } |
| |
| // Currently we resolve symlinks that were explicitly requested to be |
| // watched. Otherwise we would use LStat here. |
| stat, err := os.Stat(name) |
| if err != nil { |
| return err |
| } |
| |
| // Associate all files in the directory. |
| if stat.IsDir() { |
| err := w.handleDirectory(name, stat, true, w.associateFile) |
| if err != nil { |
| return err |
| } |
| |
| w.mu.Lock() |
| w.dirs[name] = with.op |
| w.mu.Unlock() |
| return nil |
| } |
| |
| err = w.associateFile(name, stat, true) |
| if err != nil { |
| return err |
| } |
| |
| w.mu.Lock() |
| w.watches[name] = with.op |
| w.mu.Unlock() |
| return nil |
| } |
| |
| func (w *fen) Remove(name string) error { |
| if w.isClosed() { |
| return nil |
| } |
| if !w.port.PathIsWatched(name) { |
| return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) |
| } |
| if debug { |
| fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", |
| time.Now().Format("15:04:05.000000000"), name) |
| } |
| |
| // The user has expressed an intent. Immediately remove this name from |
| // whichever watch list it might be in. If it's not in there the delete |
| // doesn't cause harm. |
| w.mu.Lock() |
| delete(w.watches, name) |
| delete(w.dirs, name) |
| w.mu.Unlock() |
| |
| stat, err := os.Stat(name) |
| if err != nil { |
| return err |
| } |
| |
| // Remove associations for every file in the directory. |
| if stat.IsDir() { |
| err := w.handleDirectory(name, stat, false, w.dissociateFile) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| err = w.port.DissociatePath(name) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // readEvents contains the main loop that runs in a goroutine watching for events. |
| func (w *fen) readEvents() { |
| // If this function returns, the watcher has been closed and we can close |
| // these channels |
| defer func() { |
| close(w.Errors) |
| close(w.Events) |
| }() |
| |
| pevents := make([]unix.PortEvent, 8) |
| for { |
| count, err := w.port.Get(pevents, 1, nil) |
| if err != nil && err != unix.ETIME { |
| // Interrupted system call (count should be 0) ignore and continue |
| if errors.Is(err, unix.EINTR) && count == 0 { |
| continue |
| } |
| // Get failed because we called w.Close() |
| if errors.Is(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 |
| } |
| |
| if debug { |
| internal.Debug(pevent.Path, pevent.Events) |
| } |
| |
| err = w.handleEvent(&pevent) |
| if !w.sendError(err) { |
| return |
| } |
| } |
| } |
| } |
| |
| func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { |
| files, err := os.ReadDir(path) |
| if err != nil { |
| return err |
| } |
| |
| // Handle all children of the directory. |
| for _, entry := range files { |
| finfo, err := entry.Info() |
| if err != nil { |
| return err |
| } |
| err = handler(filepath.Join(path, finfo.Name()), finfo, false) |
| if err != nil { |
| return err |
| } |
| } |
| |
| // And finally handle the directory itself. |
| return handler(path, stat, follow) |
| } |
| |
| // 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 *fen) handleEvent(event *unix.PortEvent) error { |
| var ( |
| events = event.Events |
| path = event.Path |
| fmode = event.Cookie.(os.FileMode) |
| reRegister = true |
| ) |
| |
| w.mu.Lock() |
| _, watchedDir := w.dirs[path] |
| _, watchedPath := w.watches[path] |
| w.mu.Unlock() |
| isWatched := watchedDir || watchedPath |
| |
| if events&unix.FILE_DELETE != 0 { |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| reRegister = false |
| } |
| if events&unix.FILE_RENAME_FROM != 0 { |
| if !w.sendEvent(path, Rename) { |
| return nil |
| } |
| // Don't keep watching the new file name |
| reRegister = false |
| } |
| if events&unix.FILE_RENAME_TO != 0 { |
| // 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. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Don't keep watching the file that was removed |
| reRegister = false |
| } |
| |
| // The file is gone, nothing left to do. |
| if !reRegister { |
| if watchedDir { |
| w.mu.Lock() |
| delete(w.dirs, path) |
| w.mu.Unlock() |
| } |
| if watchedPath { |
| w.mu.Lock() |
| delete(w.watches, path) |
| w.mu.Unlock() |
| } |
| return nil |
| } |
| |
| // If we didn't get a deletion the file still exists and we're going to have |
| // to watch it again. Let's Stat it now so that we can compare permissions |
| // and have what we need to continue watching the file |
| |
| stat, err := os.Lstat(path) |
| if err != nil { |
| // This is unexpected, but we should still emit an event. This happens |
| // most often on "rm -r" of a subdirectory inside a watched directory We |
| // get a modify event of something happening inside, but by the time we |
| // get here, the sudirectory is already gone. Clearly we were watching |
| // this path but now it is gone. Let's tell the user that it was |
| // removed. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Suppress extra write events on removed directories; they are not |
| // informative and can be confusing. |
| return nil |
| } |
| |
| // resolve symlinks that were explicitly watched as we would have at Add() |
| // time. this helps suppress spurious Chmod events on watched symlinks |
| if isWatched { |
| stat, err = os.Stat(path) |
| if err != nil { |
| // The symlink still exists, but the target is gone. Report the |
| // Remove similar to above. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Don't return the error |
| } |
| } |
| |
| if events&unix.FILE_MODIFIED != 0 { |
| if fmode.IsDir() && watchedDir { |
| if err := w.updateDirectory(path); err != nil { |
| return err |
| } |
| } else { |
| if !w.sendEvent(path, Write) { |
| return nil |
| } |
| } |
| } |
| if events&unix.FILE_ATTRIB != 0 && stat != nil { |
| // Only send Chmod if perms changed |
| if stat.Mode().Perm() != fmode.Perm() { |
| if !w.sendEvent(path, Chmod) { |
| return nil |
| } |
| } |
| } |
| |
| 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 nil |
| } |
| |
| func (w *fen) updateDirectory(path string) error { |
| // The directory was modified, so we must find unwatched entities and watch |
| // them. If something was removed from the directory, nothing will happen, |
| // as everything else should still be watched. |
| files, err := os.ReadDir(path) |
| if err != nil { |
| return err |
| } |
| |
| for _, entry := range files { |
| path := filepath.Join(path, entry.Name()) |
| if w.port.PathIsWatched(path) { |
| continue |
| } |
| |
| finfo, err := entry.Info() |
| if err != nil { |
| return err |
| } |
| err = w.associateFile(path, finfo, false) |
| if !w.sendError(err) { |
| return nil |
| } |
| if !w.sendEvent(path, Create) { |
| return nil |
| } |
| } |
| return nil |
| } |
| |
| func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error { |
| if w.isClosed() { |
| return ErrClosed |
| } |
| // This is primarily protecting the call to AssociatePath but it is |
| // important and intentional that the call to PathIsWatched is also |
| // protected by this mutex. Without this mutex, AssociatePath has been seen |
| // to error out that the path is already associated. |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| |
| if w.port.PathIsWatched(path) { |
| // Remove the old association in favor of this one If we get ENOENT, |
| // then while the x/sys/unix wrapper still thought that this path was |
| // associated, the underlying event port did not. This call will have |
| // cleared up that discrepancy. The most likely cause is that the event |
| // has fired but we haven't processed it yet. |
| err := w.port.DissociatePath(path) |
| if err != nil && !errors.Is(err, unix.ENOENT) { |
| return err |
| } |
| } |
| |
| var events int |
| if !follow { |
| // Watch symlinks themselves rather than their targets unless this entry |
| // is explicitly watched. |
| events |= unix.FILE_NOFOLLOW |
| } |
| if true { // TODO: implement withOps() |
| events |= unix.FILE_MODIFIED |
| } |
| if true { |
| events |= unix.FILE_ATTRIB |
| } |
| return w.port.AssociatePath(path, stat, events, stat.Mode()) |
| } |
| |
| func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error { |
| if !w.port.PathIsWatched(path) { |
| return nil |
| } |
| return w.port.DissociatePath(path) |
| } |
| |
| func (w *fen) WatchList() []string { |
| if w.isClosed() { |
| return nil |
| } |
| |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| |
| entries := make([]string, 0, len(w.watches)+len(w.dirs)) |
| for pathname := range w.dirs { |
| entries = append(entries, pathname) |
| } |
| for pathname := range w.watches { |
| entries = append(entries, pathname) |
| } |
| |
| return entries |
| } |
| |
| func (w *fen) xSupports(op Op) bool { |
| if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || |
| op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { |
| return false |
| } |
| return true |
| } |