github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/watcher/filenotify/poller.go (about)

     1  package filenotify
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/fsnotify/fsnotify"
    12  	"github.com/gohugoio/hugo/common/herrors"
    13  )
    14  
    15  var (
    16  	// errPollerClosed is returned when the poller is closed
    17  	errPollerClosed = errors.New("poller is closed")
    18  	// errNoSuchWatch is returned when trying to remove a watch that doesn't exist
    19  	errNoSuchWatch = errors.New("watch does not exist")
    20  )
    21  
    22  // filePoller is used to poll files for changes, especially in cases where fsnotify
    23  // can't be run (e.g. when inotify handles are exhausted)
    24  // filePoller satisfies the FileWatcher interface
    25  type filePoller struct {
    26  	// the duration between polls.
    27  	interval time.Duration
    28  	// watches is the list of files currently being polled, close the associated channel to stop the watch
    29  	watches map[string]struct{}
    30  	// Will be closed when done.
    31  	done chan struct{}
    32  	// events is the channel to listen to for watch events
    33  	events chan fsnotify.Event
    34  	// errors is the channel to listen to for watch errors
    35  	errors chan error
    36  	// mu locks the poller for modification
    37  	mu sync.Mutex
    38  	// closed is used to specify when the poller has already closed
    39  	closed bool
    40  }
    41  
    42  // Add adds a filename to the list of watches
    43  // once added the file is polled for changes in a separate goroutine
    44  func (w *filePoller) Add(name string) error {
    45  	w.mu.Lock()
    46  	defer w.mu.Unlock()
    47  
    48  	if w.closed {
    49  		return errPollerClosed
    50  	}
    51  
    52  	item, err := newItemToWatch(name)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	if item.left.FileInfo == nil {
    57  		return os.ErrNotExist
    58  	}
    59  
    60  	if w.watches == nil {
    61  		w.watches = make(map[string]struct{})
    62  	}
    63  	if _, exists := w.watches[name]; exists {
    64  		return fmt.Errorf("watch exists")
    65  	}
    66  	w.watches[name] = struct{}{}
    67  
    68  	go w.watch(item)
    69  	return nil
    70  }
    71  
    72  // Remove stops and removes watch with the specified name
    73  func (w *filePoller) Remove(name string) error {
    74  	w.mu.Lock()
    75  	defer w.mu.Unlock()
    76  	return w.remove(name)
    77  }
    78  
    79  func (w *filePoller) remove(name string) error {
    80  	if w.closed {
    81  		return errPollerClosed
    82  	}
    83  
    84  	_, exists := w.watches[name]
    85  	if !exists {
    86  		return errNoSuchWatch
    87  	}
    88  	delete(w.watches, name)
    89  	return nil
    90  }
    91  
    92  // Events returns the event channel
    93  // This is used for notifications on events about watched files
    94  func (w *filePoller) Events() <-chan fsnotify.Event {
    95  	return w.events
    96  }
    97  
    98  // Errors returns the errors channel
    99  // This is used for notifications about errors on watched files
   100  func (w *filePoller) Errors() <-chan error {
   101  	return w.errors
   102  }
   103  
   104  // Close closes the poller
   105  // All watches are stopped, removed, and the poller cannot be added to
   106  func (w *filePoller) Close() error {
   107  	w.mu.Lock()
   108  	defer w.mu.Unlock()
   109  
   110  	if w.closed {
   111  		return nil
   112  	}
   113  	w.closed = true
   114  	close(w.done)
   115  	for name := range w.watches {
   116  		w.remove(name)
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  // sendEvent publishes the specified event to the events channel
   123  func (w *filePoller) sendEvent(e fsnotify.Event) error {
   124  	select {
   125  	case w.events <- e:
   126  	case <-w.done:
   127  		return fmt.Errorf("closed")
   128  	}
   129  	return nil
   130  }
   131  
   132  // sendErr publishes the specified error to the errors channel
   133  func (w *filePoller) sendErr(e error) error {
   134  	select {
   135  	case w.errors <- e:
   136  	case <-w.done:
   137  		return fmt.Errorf("closed")
   138  	}
   139  	return nil
   140  }
   141  
   142  // watch watches item for changes until done is closed.
   143  func (w *filePoller) watch(item *itemToWatch) {
   144  	ticker := time.NewTicker(w.interval)
   145  	defer ticker.Stop()
   146  
   147  	for {
   148  		select {
   149  		case <-ticker.C:
   150  		case <-w.done:
   151  			return
   152  		}
   153  
   154  		evs, err := item.checkForChanges()
   155  		if err != nil {
   156  			if err := w.sendErr(err); err != nil {
   157  				return
   158  			}
   159  		}
   160  
   161  		item.left, item.right = item.right, item.left
   162  
   163  		for _, ev := range evs {
   164  			if err := w.sendEvent(ev); err != nil {
   165  				return
   166  			}
   167  		}
   168  
   169  	}
   170  }
   171  
   172  // recording records the state of a file or a dir.
   173  type recording struct {
   174  	os.FileInfo
   175  
   176  	// Set if FileInfo is a dir.
   177  	entries map[string]os.FileInfo
   178  }
   179  
   180  func (r *recording) clear() {
   181  	r.FileInfo = nil
   182  	if r.entries != nil {
   183  		for k := range r.entries {
   184  			delete(r.entries, k)
   185  		}
   186  	}
   187  }
   188  
   189  func (r *recording) record(filename string) error {
   190  	r.clear()
   191  
   192  	fi, err := os.Stat(filename)
   193  	if err != nil && !herrors.IsNotExist(err) {
   194  		return err
   195  	}
   196  
   197  	if fi == nil {
   198  		return nil
   199  	}
   200  
   201  	r.FileInfo = fi
   202  
   203  	// If fi is a dir, we watch the files inside that directory (not recursively).
   204  	// This matches the behaviour of fsnotity.
   205  	if fi.IsDir() {
   206  		f, err := os.Open(filename)
   207  		if err != nil {
   208  			if herrors.IsNotExist(err) {
   209  				return nil
   210  			}
   211  			return err
   212  		}
   213  		defer f.Close()
   214  
   215  		fis, err := f.Readdir(-1)
   216  		if err != nil {
   217  			if herrors.IsNotExist(err) {
   218  				return nil
   219  			}
   220  			return err
   221  		}
   222  
   223  		for _, fi := range fis {
   224  			r.entries[fi.Name()] = fi
   225  		}
   226  	}
   227  
   228  	return nil
   229  }
   230  
   231  // itemToWatch may be a file or a dir.
   232  type itemToWatch struct {
   233  	// Full path to the filename.
   234  	filename string
   235  
   236  	// Snapshots of the stat state of this file or dir.
   237  	left  *recording
   238  	right *recording
   239  }
   240  
   241  func newItemToWatch(filename string) (*itemToWatch, error) {
   242  	r := &recording{
   243  		entries: make(map[string]os.FileInfo),
   244  	}
   245  	err := r.record(filename)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	return &itemToWatch{filename: filename, left: r}, nil
   251  
   252  }
   253  
   254  func (item *itemToWatch) checkForChanges() ([]fsnotify.Event, error) {
   255  	if item.right == nil {
   256  		item.right = &recording{
   257  			entries: make(map[string]os.FileInfo),
   258  		}
   259  	}
   260  
   261  	err := item.right.record(item.filename)
   262  	if err != nil && !herrors.IsNotExist(err) {
   263  		return nil, err
   264  	}
   265  
   266  	dirOp := checkChange(item.left.FileInfo, item.right.FileInfo)
   267  
   268  	if dirOp != 0 {
   269  		evs := []fsnotify.Event{{Op: dirOp, Name: item.filename}}
   270  		return evs, nil
   271  	}
   272  
   273  	if item.left.FileInfo == nil || !item.left.IsDir() {
   274  		// Done.
   275  		return nil, nil
   276  	}
   277  
   278  	leftIsIn := false
   279  	left, right := item.left.entries, item.right.entries
   280  	if len(right) > len(left) {
   281  		left, right = right, left
   282  		leftIsIn = true
   283  	}
   284  
   285  	var evs []fsnotify.Event
   286  
   287  	for name, fi1 := range left {
   288  		fi2 := right[name]
   289  		fil, fir := fi1, fi2
   290  		if leftIsIn {
   291  			fil, fir = fir, fil
   292  		}
   293  		op := checkChange(fil, fir)
   294  		if op != 0 {
   295  			evs = append(evs, fsnotify.Event{Op: op, Name: filepath.Join(item.filename, name)})
   296  		}
   297  
   298  	}
   299  
   300  	return evs, nil
   301  
   302  }
   303  
   304  func checkChange(fi1, fi2 os.FileInfo) fsnotify.Op {
   305  	if fi1 == nil && fi2 != nil {
   306  		return fsnotify.Create
   307  	}
   308  	if fi1 != nil && fi2 == nil {
   309  		return fsnotify.Remove
   310  	}
   311  	if fi1 == nil && fi2 == nil {
   312  		return 0
   313  	}
   314  	if fi1.IsDir() || fi2.IsDir() {
   315  		return 0
   316  	}
   317  	if fi1.Mode() != fi2.Mode() {
   318  		return fsnotify.Chmod
   319  	}
   320  	if fi1.ModTime() != fi2.ModTime() || fi1.Size() != fi2.Size() {
   321  		return fsnotify.Write
   322  	}
   323  
   324  	return 0
   325  }