backend_fen.go

  1//go:build solaris
  2
  3// FEN backend for illumos (supported) and Solaris (untested, but should work).
  4//
  5// See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create
  6
  7package fsnotify
  8
  9import (
 10	"errors"
 11	"fmt"
 12	"os"
 13	"path/filepath"
 14	"sync"
 15	"time"
 16
 17	"github.com/fsnotify/fsnotify/internal"
 18	"golang.org/x/sys/unix"
 19)
 20
 21type fen struct {
 22	Events chan Event
 23	Errors chan error
 24
 25	mu      sync.Mutex
 26	port    *unix.EventPort
 27	done    chan struct{} // Channel for sending a "quit message" to the reader goroutine
 28	dirs    map[string]Op // Explicitly watched directories
 29	watches map[string]Op // Explicitly watched non-directories
 30}
 31
 32func newBackend(ev chan Event, errs chan error) (backend, error) {
 33	return newBufferedBackend(0, ev, errs)
 34}
 35
 36func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
 37	w := &fen{
 38		Events:  ev,
 39		Errors:  errs,
 40		dirs:    make(map[string]Op),
 41		watches: make(map[string]Op),
 42		done:    make(chan struct{}),
 43	}
 44
 45	var err error
 46	w.port, err = unix.NewEventPort()
 47	if err != nil {
 48		return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
 49	}
 50
 51	go w.readEvents()
 52	return w, nil
 53}
 54
 55// sendEvent attempts to send an event to the user, returning true if the event
 56// was put in the channel successfully and false if the watcher has been closed.
 57func (w *fen) sendEvent(name string, op Op) (sent bool) {
 58	select {
 59	case <-w.done:
 60		return false
 61	case w.Events <- Event{Name: name, Op: op}:
 62		return true
 63	}
 64}
 65
 66// sendError attempts to send an error to the user, returning true if the error
 67// was put in the channel successfully and false if the watcher has been closed.
 68func (w *fen) sendError(err error) (sent bool) {
 69	if err == nil {
 70		return true
 71	}
 72	select {
 73	case <-w.done:
 74		return false
 75	case w.Errors <- err:
 76		return true
 77	}
 78}
 79
 80func (w *fen) isClosed() bool {
 81	select {
 82	case <-w.done:
 83		return true
 84	default:
 85		return false
 86	}
 87}
 88
 89func (w *fen) Close() error {
 90	// Take the lock used by associateFile to prevent lingering events from
 91	// being processed after the close
 92	w.mu.Lock()
 93	defer w.mu.Unlock()
 94	if w.isClosed() {
 95		return nil
 96	}
 97	close(w.done)
 98	return w.port.Close()
 99}
100
101func (w *fen) Add(name string) error { return w.AddWith(name) }
102
103func (w *fen) AddWith(name string, opts ...addOpt) error {
104	if w.isClosed() {
105		return ErrClosed
106	}
107	if debug {
108		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  AddWith(%q)\n",
109			time.Now().Format("15:04:05.000000000"), name)
110	}
111
112	with := getOptions(opts...)
113	if !w.xSupports(with.op) {
114		return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
115	}
116
117	// Currently we resolve symlinks that were explicitly requested to be
118	// watched. Otherwise we would use LStat here.
119	stat, err := os.Stat(name)
120	if err != nil {
121		return err
122	}
123
124	// Associate all files in the directory.
125	if stat.IsDir() {
126		err := w.handleDirectory(name, stat, true, w.associateFile)
127		if err != nil {
128			return err
129		}
130
131		w.mu.Lock()
132		w.dirs[name] = with.op
133		w.mu.Unlock()
134		return nil
135	}
136
137	err = w.associateFile(name, stat, true)
138	if err != nil {
139		return err
140	}
141
142	w.mu.Lock()
143	w.watches[name] = with.op
144	w.mu.Unlock()
145	return nil
146}
147
148func (w *fen) Remove(name string) error {
149	if w.isClosed() {
150		return nil
151	}
152	if !w.port.PathIsWatched(name) {
153		return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
154	}
155	if debug {
156		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  Remove(%q)\n",
157			time.Now().Format("15:04:05.000000000"), name)
158	}
159
160	// The user has expressed an intent. Immediately remove this name from
161	// whichever watch list it might be in. If it's not in there the delete
162	// doesn't cause harm.
163	w.mu.Lock()
164	delete(w.watches, name)
165	delete(w.dirs, name)
166	w.mu.Unlock()
167
168	stat, err := os.Stat(name)
169	if err != nil {
170		return err
171	}
172
173	// Remove associations for every file in the directory.
174	if stat.IsDir() {
175		err := w.handleDirectory(name, stat, false, w.dissociateFile)
176		if err != nil {
177			return err
178		}
179		return nil
180	}
181
182	err = w.port.DissociatePath(name)
183	if err != nil {
184		return err
185	}
186
187	return nil
188}
189
190// readEvents contains the main loop that runs in a goroutine watching for events.
191func (w *fen) readEvents() {
192	// If this function returns, the watcher has been closed and we can close
193	// these channels
194	defer func() {
195		close(w.Errors)
196		close(w.Events)
197	}()
198
199	pevents := make([]unix.PortEvent, 8)
200	for {
201		count, err := w.port.Get(pevents, 1, nil)
202		if err != nil && err != unix.ETIME {
203			// Interrupted system call (count should be 0) ignore and continue
204			if errors.Is(err, unix.EINTR) && count == 0 {
205				continue
206			}
207			// Get failed because we called w.Close()
208			if errors.Is(err, unix.EBADF) && w.isClosed() {
209				return
210			}
211			// There was an error not caused by calling w.Close()
212			if !w.sendError(err) {
213				return
214			}
215		}
216
217		p := pevents[:count]
218		for _, pevent := range p {
219			if pevent.Source != unix.PORT_SOURCE_FILE {
220				// Event from unexpected source received; should never happen.
221				if !w.sendError(errors.New("Event from unexpected source received")) {
222					return
223				}
224				continue
225			}
226
227			if debug {
228				internal.Debug(pevent.Path, pevent.Events)
229			}
230
231			err = w.handleEvent(&pevent)
232			if !w.sendError(err) {
233				return
234			}
235		}
236	}
237}
238
239func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
240	files, err := os.ReadDir(path)
241	if err != nil {
242		return err
243	}
244
245	// Handle all children of the directory.
246	for _, entry := range files {
247		finfo, err := entry.Info()
248		if err != nil {
249			return err
250		}
251		err = handler(filepath.Join(path, finfo.Name()), finfo, false)
252		if err != nil {
253			return err
254		}
255	}
256
257	// And finally handle the directory itself.
258	return handler(path, stat, follow)
259}
260
261// handleEvent might need to emit more than one fsnotify event if the events
262// bitmap matches more than one event type (e.g. the file was both modified and
263// had the attributes changed between when the association was created and the
264// when event was returned)
265func (w *fen) handleEvent(event *unix.PortEvent) error {
266	var (
267		events     = event.Events
268		path       = event.Path
269		fmode      = event.Cookie.(os.FileMode)
270		reRegister = true
271	)
272
273	w.mu.Lock()
274	_, watchedDir := w.dirs[path]
275	_, watchedPath := w.watches[path]
276	w.mu.Unlock()
277	isWatched := watchedDir || watchedPath
278
279	if events&unix.FILE_DELETE != 0 {
280		if !w.sendEvent(path, Remove) {
281			return nil
282		}
283		reRegister = false
284	}
285	if events&unix.FILE_RENAME_FROM != 0 {
286		if !w.sendEvent(path, Rename) {
287			return nil
288		}
289		// Don't keep watching the new file name
290		reRegister = false
291	}
292	if events&unix.FILE_RENAME_TO != 0 {
293		// We don't report a Rename event for this case, because Rename events
294		// are interpreted as referring to the _old_ name of the file, and in
295		// this case the event would refer to the new name of the file. This
296		// type of rename event is not supported by fsnotify.
297
298		// inotify reports a Remove event in this case, so we simulate this
299		// here.
300		if !w.sendEvent(path, Remove) {
301			return nil
302		}
303		// Don't keep watching the file that was removed
304		reRegister = false
305	}
306
307	// The file is gone, nothing left to do.
308	if !reRegister {
309		if watchedDir {
310			w.mu.Lock()
311			delete(w.dirs, path)
312			w.mu.Unlock()
313		}
314		if watchedPath {
315			w.mu.Lock()
316			delete(w.watches, path)
317			w.mu.Unlock()
318		}
319		return nil
320	}
321
322	// If we didn't get a deletion the file still exists and we're going to have
323	// to watch it again. Let's Stat it now so that we can compare permissions
324	// and have what we need to continue watching the file
325
326	stat, err := os.Lstat(path)
327	if err != nil {
328		// This is unexpected, but we should still emit an event. This happens
329		// most often on "rm -r" of a subdirectory inside a watched directory We
330		// get a modify event of something happening inside, but by the time we
331		// get here, the sudirectory is already gone. Clearly we were watching
332		// this path but now it is gone. Let's tell the user that it was
333		// removed.
334		if !w.sendEvent(path, Remove) {
335			return nil
336		}
337		// Suppress extra write events on removed directories; they are not
338		// informative and can be confusing.
339		return nil
340	}
341
342	// resolve symlinks that were explicitly watched as we would have at Add()
343	// time. this helps suppress spurious Chmod events on watched symlinks
344	if isWatched {
345		stat, err = os.Stat(path)
346		if err != nil {
347			// The symlink still exists, but the target is gone. Report the
348			// Remove similar to above.
349			if !w.sendEvent(path, Remove) {
350				return nil
351			}
352			// Don't return the error
353		}
354	}
355
356	if events&unix.FILE_MODIFIED != 0 {
357		if fmode.IsDir() && watchedDir {
358			if err := w.updateDirectory(path); err != nil {
359				return err
360			}
361		} else {
362			if !w.sendEvent(path, Write) {
363				return nil
364			}
365		}
366	}
367	if events&unix.FILE_ATTRIB != 0 && stat != nil {
368		// Only send Chmod if perms changed
369		if stat.Mode().Perm() != fmode.Perm() {
370			if !w.sendEvent(path, Chmod) {
371				return nil
372			}
373		}
374	}
375
376	if stat != nil {
377		// If we get here, it means we've hit an event above that requires us to
378		// continue watching the file or directory
379		return w.associateFile(path, stat, isWatched)
380	}
381	return nil
382}
383
384func (w *fen) updateDirectory(path string) error {
385	// The directory was modified, so we must find unwatched entities and watch
386	// them. If something was removed from the directory, nothing will happen,
387	// as everything else should still be watched.
388	files, err := os.ReadDir(path)
389	if err != nil {
390		return err
391	}
392
393	for _, entry := range files {
394		path := filepath.Join(path, entry.Name())
395		if w.port.PathIsWatched(path) {
396			continue
397		}
398
399		finfo, err := entry.Info()
400		if err != nil {
401			return err
402		}
403		err = w.associateFile(path, finfo, false)
404		if !w.sendError(err) {
405			return nil
406		}
407		if !w.sendEvent(path, Create) {
408			return nil
409		}
410	}
411	return nil
412}
413
414func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
415	if w.isClosed() {
416		return ErrClosed
417	}
418	// This is primarily protecting the call to AssociatePath but it is
419	// important and intentional that the call to PathIsWatched is also
420	// protected by this mutex. Without this mutex, AssociatePath has been seen
421	// to error out that the path is already associated.
422	w.mu.Lock()
423	defer w.mu.Unlock()
424
425	if w.port.PathIsWatched(path) {
426		// Remove the old association in favor of this one If we get ENOENT,
427		// then while the x/sys/unix wrapper still thought that this path was
428		// associated, the underlying event port did not. This call will have
429		// cleared up that discrepancy. The most likely cause is that the event
430		// has fired but we haven't processed it yet.
431		err := w.port.DissociatePath(path)
432		if err != nil && !errors.Is(err, unix.ENOENT) {
433			return err
434		}
435	}
436
437	var events int
438	if !follow {
439		// Watch symlinks themselves rather than their targets unless this entry
440		// is explicitly watched.
441		events |= unix.FILE_NOFOLLOW
442	}
443	if true { // TODO: implement withOps()
444		events |= unix.FILE_MODIFIED
445	}
446	if true {
447		events |= unix.FILE_ATTRIB
448	}
449	return w.port.AssociatePath(path, stat, events, stat.Mode())
450}
451
452func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
453	if !w.port.PathIsWatched(path) {
454		return nil
455	}
456	return w.port.DissociatePath(path)
457}
458
459func (w *fen) WatchList() []string {
460	if w.isClosed() {
461		return nil
462	}
463
464	w.mu.Lock()
465	defer w.mu.Unlock()
466
467	entries := make([]string, 0, len(w.watches)+len(w.dirs))
468	for pathname := range w.dirs {
469		entries = append(entries, pathname)
470	}
471	for pathname := range w.watches {
472		entries = append(entries, pathname)
473	}
474
475	return entries
476}
477
478func (w *fen) xSupports(op Op) bool {
479	if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
480		op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
481		return false
482	}
483	return true
484}