github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/watcher/filenotify/poller.go (about)

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