~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
263 lines · 4.3 KB Raw History
package fswatch

import (
	"errors"
	"path/filepath"
	"sync"

	"github.com/fsnotify/fsnotify"
)

var ErrClosed = errors.New("fswatch: the watch is closed")

type Watcher struct {
	mu     sync.Mutex
	w      *fsnotify.Watcher
	refs   map[string]int
	subs   map[*Sub]struct{}
	closed bool
}

func New() *Watcher {
	return &Watcher{refs: map[string]int{}, subs: map[*Sub]struct{}{}}
}

var shared = New()

func Shared() *Watcher { return shared }

type item struct {
	ev  fsnotify.Event
	err error
}

type Sub struct {
	parent  *Watcher
	paths   map[string]bool
	events  chan fsnotify.Event
	errors  chan error
	wake    chan struct{}
	done    chan struct{}
	once    sync.Once
	mu      sync.Mutex
	queue   []item
	pending map[fsnotify.Event]bool
	failed  bool
}

func (w *Watcher) Subscribe(paths ...string) (*Sub, error) {
	s := &Sub{parent: w, paths: map[string]bool{}, events: make(chan fsnotify.Event), errors: make(chan error),
		wake: make(chan struct{}, 1), done: make(chan struct{}), pending: map[fsnotify.Event]bool{}}
	w.mu.Lock()
	if w.closed {
		w.mu.Unlock()
		return nil, ErrClosed
	}
	w.subs[s] = struct{}{}
	w.mu.Unlock()
	go s.pump()
	for _, p := range paths {
		if err := s.Add(p); err != nil {
			s.Close()
			return nil, err
		}
	}
	return s, nil
}

func (w *Watcher) open() (*fsnotify.Watcher, error) {
	if w.w != nil {
		return w.w, nil
	}
	fw, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}
	w.w = fw
	go w.run(fw)
	return fw, nil
}

func (w *Watcher) run(fw *fsnotify.Watcher) {
	for {
		select {
		case ev, ok := <-fw.Events:
			if !ok {
				return
			}
			w.dispatch(ev)
		case err, ok := <-fw.Errors:
			if !ok {
				return
			}
			w.broadcast(err)
		}
	}
}

func (w *Watcher) dispatch(ev fsnotify.Event) {
	name := filepath.Clean(ev.Name)
	dir := filepath.Dir(name)
	w.mu.Lock()
	defer w.mu.Unlock()
	for s := range w.subs {
		if s.paths[name] || s.paths[dir] {
			s.enqueue(item{ev: ev})
		}
	}
}

func (w *Watcher) broadcast(err error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	for s := range w.subs {
		s.enqueue(item{err: err})
	}
}

func (w *Watcher) release(path string) {
	w.refs[path]--
	if w.refs[path] > 0 {
		return
	}
	delete(w.refs, path)
	if w.w != nil {
		_ = w.w.Remove(path)
	}
}

func (w *Watcher) Close() error {
	w.mu.Lock()
	if w.closed {
		w.mu.Unlock()
		return nil
	}
	w.closed = true
	subs := make([]*Sub, 0, len(w.subs))
	for s := range w.subs {
		subs = append(subs, s)
	}
	fw := w.w
	w.w = nil
	w.mu.Unlock()
	for _, s := range subs {
		s.Close()
	}
	if fw == nil {
		return nil
	}
	return fw.Close()
}

func (s *Sub) Events() <-chan fsnotify.Event { return s.events }

func (s *Sub) Errors() <-chan error { return s.errors }

func (s *Sub) Add(path string) error {
	path = filepath.Clean(path)
	w := s.parent
	w.mu.Lock()
	defer w.mu.Unlock()
	if _, live := w.subs[s]; !live || w.closed {
		return ErrClosed
	}
	if s.paths[path] {
		return nil
	}
	fw, err := w.open()
	if err != nil {
		return err
	}
	if err := fw.Add(path); err != nil {
		return err
	}
	s.paths[path] = true
	w.refs[path]++
	return nil
}

func (s *Sub) Remove(path string) {
	path = filepath.Clean(path)
	w := s.parent
	w.mu.Lock()
	defer w.mu.Unlock()
	if !s.paths[path] {
		return
	}
	delete(s.paths, path)
	w.release(path)
}

func (s *Sub) Close() {
	s.once.Do(func() {
		w := s.parent
		w.mu.Lock()
		delete(w.subs, s)
		for p := range s.paths {
			w.release(p)
		}
		s.paths = map[string]bool{}
		w.mu.Unlock()
		close(s.done)
	})
}

func (s *Sub) enqueue(it item) {
	s.mu.Lock()
	switch {
	case it.err != nil && s.failed:
	case it.err != nil:
		s.failed = true
		s.queue = append(s.queue, it)
	case s.pending[it.ev]:
	default:
		s.pending[it.ev] = true
		s.queue = append(s.queue, it)
	}
	s.mu.Unlock()
	select {
	case s.wake <- struct{}{}:
	default:
	}
}

func (s *Sub) pump() {
	defer close(s.errors)
	defer close(s.events)
	for {
		s.mu.Lock()
		if len(s.queue) == 0 {
			s.mu.Unlock()
			select {
			case <-s.wake:
				continue
			case <-s.done:
				return
			}
		}
		it := s.queue[0]
		s.queue = s.queue[1:]
		if len(s.queue) == 0 {
			s.queue = nil
		}
		if it.err != nil {
			s.failed = false
		} else {
			delete(s.pending, it.ev)
		}
		s.mu.Unlock()
		if it.err != nil {
			select {
			case s.errors <- it.err:
			case <-s.done:
				return
			}
			continue
		}
		select {
		case s.events <- it.ev:
		case <-s.done:
			return
		}
	}
}