mirror of
https://github.com/NotAShelf/catApi.git
synced 2025-02-23 11:59:47 +00:00
484 lines
12 KiB
Go
484 lines
12 KiB
Go
//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
|
|
}
|